TerrainSystemTest.cpp 80 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553
  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. #include <SurfaceData/Utility/SurfaceDataUtility.h>
  22. #include <random>
  23. using ::testing::AtLeast;
  24. using ::testing::FloatNear;
  25. using ::testing::FloatEq;
  26. using ::testing::IsFalse;
  27. using ::testing::Ne;
  28. using ::testing::NiceMock;
  29. using ::testing::Return;
  30. using ::testing::SetArgReferee;
  31. namespace UnitTest
  32. {
  33. class TerrainSystemTest
  34. : public TerrainBaseFixture
  35. , public ::testing::Test
  36. {
  37. protected:
  38. // Defines a structure for defining both an XY position and the expected height for that position.
  39. struct HeightTestPoint
  40. {
  41. AZ::Vector2 m_testLocation = AZ::Vector2::CreateZero();
  42. float m_expectedHeight = 0.0f;
  43. };
  44. struct NormalTestPoint
  45. {
  46. AZ::Vector2 m_testLocation = AZ::Vector2::CreateZero();
  47. AZ::Vector3 m_expectedNormal = AZ::Vector3::CreateZero();
  48. };
  49. struct HeightTestRegionPoints
  50. {
  51. size_t m_xIndex;
  52. size_t m_yIndex;
  53. float m_expectedHeight;
  54. AZ::Vector2 m_testLocation = AZ::Vector2::CreateZero();
  55. };
  56. struct NormalTestRegionPoints
  57. {
  58. size_t m_xIndex;
  59. size_t m_yIndex;
  60. AZ::Vector3 m_expectedNormal = AZ::Vector3::CreateZero();
  61. AZ::Vector2 m_testLocation = AZ::Vector2::CreateZero();
  62. };
  63. AZStd::unique_ptr<NiceMock<UnitTest::MockBoxShapeComponentRequests>> m_boxShapeRequests;
  64. AZStd::unique_ptr<NiceMock<UnitTest::MockShapeComponentRequests>> m_shapeRequests;
  65. AZStd::unique_ptr<NiceMock<UnitTest::MockTerrainAreaHeightRequests>> m_terrainAreaHeightRequests;
  66. AZStd::unique_ptr<NiceMock<UnitTest::MockTerrainAreaSurfaceRequestBus>> m_terrainAreaSurfaceRequests;
  67. void SetUp() override
  68. {
  69. SetupCoreSystems();
  70. }
  71. void TearDown() override
  72. {
  73. m_boxShapeRequests.reset();
  74. m_shapeRequests.reset();
  75. m_terrainAreaHeightRequests.reset();
  76. m_terrainAreaSurfaceRequests.reset();
  77. TearDownCoreSystems();
  78. }
  79. AZStd::unique_ptr<AZ::Entity> CreateAndActivateMockTerrainLayerSpawner(
  80. const AZ::Aabb& spawnerBox, const AZStd::function<void(AZ::Vector3& position, bool& terrainExists)>& mockHeights)
  81. {
  82. // Create the base entity with a mock box shape, Terrain Layer Spawner, and height provider.
  83. auto entity = CreateEntity();
  84. entity->CreateComponent<UnitTest::MockAxisAlignedBoxShapeComponent>();
  85. entity->CreateComponent<Terrain::TerrainLayerSpawnerComponent>();
  86. m_boxShapeRequests = AZStd::make_unique<NiceMock<UnitTest::MockBoxShapeComponentRequests>>(entity->GetId());
  87. m_shapeRequests = AZStd::make_unique<NiceMock<UnitTest::MockShapeComponentRequests>>(entity->GetId());
  88. // Set up the box shape to return whatever spawnerBox was passed in.
  89. ON_CALL(*m_shapeRequests, GetEncompassingAabb).WillByDefault(Return(spawnerBox));
  90. // Set up a mock height provider to use the passed-in mock height function to generate a height.
  91. m_terrainAreaHeightRequests = AZStd::make_unique<NiceMock<UnitTest::MockTerrainAreaHeightRequests>>(entity->GetId());
  92. ON_CALL(*m_terrainAreaHeightRequests, GetHeight)
  93. .WillByDefault(
  94. [mockHeights](const AZ::Vector3& inPosition, AZ::Vector3& outPosition, bool& terrainExists)
  95. {
  96. // By default, set the outPosition to the input position and terrain to always exist.
  97. outPosition = inPosition;
  98. terrainExists = true;
  99. // Let the test function modify these values based on the needs of the specific test.
  100. mockHeights(outPosition, terrainExists);
  101. });
  102. ON_CALL(*m_terrainAreaHeightRequests, GetHeights)
  103. .WillByDefault(
  104. [mockHeights](AZStd::span<AZ::Vector3> inOutPositionList, AZStd::span<bool> terrainExistsList)
  105. {
  106. for (int i = 0; i < inOutPositionList.size(); i++)
  107. {
  108. mockHeights(inOutPositionList[i], terrainExistsList[i]);
  109. }
  110. });
  111. ActivateEntity(entity.get());
  112. return entity;
  113. }
  114. void SetupSurfaceWeightMocks(AZ::Entity* entity, AzFramework::SurfaceData::SurfaceTagWeightList& expectedTags)
  115. {
  116. const SurfaceData::SurfaceTag tag1 = SurfaceData::SurfaceTag("tag1");
  117. const SurfaceData::SurfaceTag tag2 = SurfaceData::SurfaceTag("tag2");
  118. const SurfaceData::SurfaceTag tag3 = SurfaceData::SurfaceTag("tag3");
  119. AzFramework::SurfaceData::SurfaceTagWeight tagWeight1;
  120. tagWeight1.m_surfaceType = tag1;
  121. tagWeight1.m_weight = 1.0f;
  122. expectedTags.push_back(tagWeight1);
  123. AzFramework::SurfaceData::SurfaceTagWeight tagWeight2;
  124. tagWeight2.m_surfaceType = tag2;
  125. tagWeight2.m_weight = 0.7f;
  126. expectedTags.push_back(tagWeight2);
  127. AzFramework::SurfaceData::SurfaceTagWeight tagWeight3;
  128. tagWeight3.m_surfaceType = tag3;
  129. tagWeight3.m_weight = 0.3f;
  130. expectedTags.push_back(tagWeight3);
  131. auto mockGetSurfaceWeights = [tagWeight1, tagWeight2, tagWeight3](
  132. const AZ::Vector3& position,
  133. AzFramework::SurfaceData::SurfaceTagWeightList& surfaceWeights)
  134. {
  135. surfaceWeights.clear();
  136. float absYPos = fabsf(position.GetY());
  137. if (absYPos < 1.0f)
  138. {
  139. surfaceWeights.push_back(tagWeight1);
  140. }
  141. else if(absYPos < 2.0f)
  142. {
  143. surfaceWeights.push_back(tagWeight2);
  144. }
  145. else
  146. {
  147. surfaceWeights.push_back(tagWeight3);
  148. }
  149. };
  150. m_terrainAreaSurfaceRequests = AZStd::make_unique<NiceMock<UnitTest::MockTerrainAreaSurfaceRequestBus>>(entity->GetId());
  151. ON_CALL(*m_terrainAreaSurfaceRequests, GetSurfaceWeights).WillByDefault(mockGetSurfaceWeights);
  152. ON_CALL(*m_terrainAreaSurfaceRequests, GetSurfaceWeightsFromList).WillByDefault(
  153. [mockGetSurfaceWeights](
  154. AZStd::span<const AZ::Vector3> inPositionList,
  155. AZStd::span<AzFramework::SurfaceData::SurfaceTagWeightList> outSurfaceWeightsList)
  156. {
  157. for (size_t i = 0; i < inPositionList.size(); i++)
  158. {
  159. mockGetSurfaceWeights(inPositionList[i], outSurfaceWeightsList[i]);
  160. }
  161. }
  162. );
  163. }
  164. void TestNormals(const AZStd::span<const NormalTestPoint>& testPoints,
  165. AzFramework::Terrain::TerrainDataRequests::Sampler sampler,
  166. AZStd::function<void(AZ::Vector3&position, bool&terrainExists)> heightMockFunction)
  167. {
  168. // Create and activate the terrain system with the same testing defaults for world bounds and query resolutions for
  169. // all of our normals tests.
  170. constexpr float queryResolution = 1.0f;
  171. const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -20.0f, 10.0f, 10.0f, 20.0f);
  172. auto entity = CreateAndActivateMockTerrainLayerSpawner(spawnerBox, heightMockFunction);
  173. auto terrainSystem = CreateAndActivateTerrainSystem(queryResolution);
  174. for (auto& testPoint : testPoints)
  175. {
  176. bool terrainExists = false;
  177. AZ::Vector3 normal = terrainSystem->GetNormal(AZ::Vector3(testPoint.m_testLocation), sampler, &terrainExists);
  178. EXPECT_TRUE(terrainExists);
  179. EXPECT_THAT(normal, UnitTest::IsClose(testPoint.m_expectedNormal));
  180. }
  181. }
  182. };
  183. TEST_F(TerrainSystemTest, TrivialCreateDestroy)
  184. {
  185. // Trivially verify that the terrain system can successfully be constructed and destructed without errors.
  186. auto terrainSystem = AZStd::make_unique<Terrain::TerrainSystem>();
  187. }
  188. TEST_F(TerrainSystemTest, TrivialActivateDeactivate)
  189. {
  190. // Verify that the terrain system can be activated and deactivated without errors.
  191. auto terrainSystem = AZStd::make_unique<Terrain::TerrainSystem>();
  192. terrainSystem->Activate();
  193. terrainSystem->Deactivate();
  194. }
  195. TEST_F(TerrainSystemTest, CreateEventsCalledOnActivation)
  196. {
  197. // Verify that when the terrain system is activated, the OnTerrainDataCreate* ebus notifications are generated.
  198. NiceMock<UnitTest::MockTerrainDataNotificationListener> mockTerrainListener;
  199. EXPECT_CALL(mockTerrainListener, OnTerrainDataCreateBegin()).Times(AtLeast(1));
  200. EXPECT_CALL(mockTerrainListener, OnTerrainDataCreateEnd()).Times(AtLeast(1));
  201. auto terrainSystem = AZStd::make_unique<Terrain::TerrainSystem>();
  202. terrainSystem->Activate();
  203. }
  204. TEST_F(TerrainSystemTest, DestroyEventsCalledOnDeactivation)
  205. {
  206. // Verify that when the terrain system is deactivated, the OnTerrainDataDestroy* ebus notifications are generated.
  207. NiceMock<UnitTest::MockTerrainDataNotificationListener> mockTerrainListener;
  208. EXPECT_CALL(mockTerrainListener, OnTerrainDataDestroyBegin()).Times(AtLeast(1));
  209. EXPECT_CALL(mockTerrainListener, OnTerrainDataDestroyEnd()).Times(AtLeast(1));
  210. auto terrainSystem = AZStd::make_unique<Terrain::TerrainSystem>();
  211. terrainSystem->Activate();
  212. terrainSystem->Deactivate();
  213. }
  214. TEST_F(TerrainSystemTest, TerrainDoesNotExistWhenNoTerrainLayerSpawnersAreRegistered)
  215. {
  216. // For the terrain system, terrain should only exist where terrain layer spawners are present.
  217. // Verify that in the active terrain system, if there are no terrain layer spawners, any arbitrary point
  218. // will return false for terrainExists, returns a height equal to the min world bounds of the terrain system, and returns
  219. // a normal facing up the Z axis.
  220. // Create and activate the terrain system with our testing defaults for world bounds and query resolution.
  221. auto terrainSystem = CreateAndActivateTerrainSystem();
  222. AzFramework::Terrain::FloatRange heightBounds;
  223. AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
  224. heightBounds, &AzFramework::Terrain::TerrainDataRequestBus::Events::GetTerrainHeightBounds);
  225. // Create an arbitrary world bounds to test since the bounds of the terrain system will be 0 with no terrain areas.
  226. AZ::Aabb worldBounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-10.0f, -10.0f, -10.0f), AZ::Vector3(10.0f, 10.0f, 10.0f));
  227. // Loop through several points within the world bounds, including on the edges, and verify that they all return false for
  228. // terrainExists with default heights and normals.
  229. for (float y = worldBounds.GetMin().GetY(); y <= worldBounds.GetMax().GetY(); y += (worldBounds.GetExtents().GetY() / 4.0f))
  230. {
  231. for (float x = worldBounds.GetMin().GetX(); x <= worldBounds.GetMax().GetX(); x += (worldBounds.GetExtents().GetX() / 4.0f))
  232. {
  233. AZ::Vector3 position(x, y, 0.0f);
  234. bool terrainExists = true;
  235. float height =
  236. terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists);
  237. EXPECT_FALSE(terrainExists);
  238. EXPECT_FLOAT_EQ(height, heightBounds.m_min);
  239. terrainExists = true;
  240. AZ::Vector3 normal =
  241. terrainSystem->GetNormal(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists);
  242. EXPECT_FALSE(terrainExists);
  243. EXPECT_EQ(normal, AZ::Vector3::CreateAxisZ());
  244. bool isHole = terrainSystem->GetIsHoleFromFloats(
  245. position.GetX(), position.GetY(), AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT);
  246. EXPECT_TRUE(isHole);
  247. }
  248. }
  249. }
  250. TEST_F(TerrainSystemTest, TerrainExistsOnlyWithinTerrainLayerSpawnerBounds)
  251. {
  252. // Verify that the presence of a TerrainLayerSpawner causes terrain to exist in (and *only* in) the box where the
  253. // TerrainLayerSpawner is defined. The box is min-inclusive-max-exclusive, so points should *not* exist on the max edge of the box.
  254. // The terrain system should only query Heights from the TerrainAreaHeightRequest bus within the
  255. // TerrainLayerSpawner region, and so those values should only get returned from GetHeight for queries inside that region.
  256. // Create a mock terrain layer spawner that uses a box of (0,0,5) - (10,10,15) and always returns a height of 5.
  257. constexpr float spawnerHeight = 5.0f;
  258. const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 5.0f, 10.0f, 10.0f, 15.0f);
  259. auto entity = CreateAndActivateMockTerrainLayerSpawner(
  260. spawnerBox,
  261. [](AZ::Vector3& position, bool& terrainExists)
  262. {
  263. position.SetZ(spawnerHeight);
  264. terrainExists = true;
  265. });
  266. // Verify that terrain exists within the layer spawner bounds, and doesn't exist outside of it.
  267. // Create and activate the terrain system with our testing defaults for world bounds and query resolution.
  268. auto terrainSystem = CreateAndActivateTerrainSystem();
  269. // Create a box that's twice as big as the layer spawner box. Loop through it and verify that points within the layer box contain
  270. // terrain and the expected height & normal values, and points outside the layer box don't contain terrain.
  271. const AZ::Aabb encompassingBox = AZ::Aabb::CreateFromMinMax(
  272. spawnerBox.GetMin() - (spawnerBox.GetExtents() / 2.0f), spawnerBox.GetMax() + (spawnerBox.GetExtents() / 2.0f));
  273. for (float y = encompassingBox.GetMin().GetY(); y < encompassingBox.GetMax().GetY(); y += 1.0f)
  274. {
  275. for (float x = encompassingBox.GetMin().GetX(); x < encompassingBox.GetMax().GetX(); x += 1.0f)
  276. {
  277. AZ::Vector3 position(x, y, 0.0f);
  278. bool heightQueryTerrainExists = false;
  279. float height = terrainSystem->GetHeight(
  280. position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &heightQueryTerrainExists);
  281. bool isHole = terrainSystem->GetIsHoleFromFloats(
  282. position.GetX(), position.GetY(), AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT);
  283. // Verify that the point either should or shouldn't appear on the box, taking min-inclusive-max-exclusive box ranges
  284. // into account.
  285. if (SurfaceData::AabbContains2DMaxExclusive(spawnerBox,
  286. AZ::Vector3(position.GetX(), position.GetY(), spawnerBox.GetMin().GetZ())))
  287. {
  288. EXPECT_TRUE(heightQueryTerrainExists);
  289. EXPECT_FALSE(isHole);
  290. EXPECT_FLOAT_EQ(height, spawnerHeight);
  291. }
  292. else
  293. {
  294. EXPECT_FALSE(heightQueryTerrainExists);
  295. EXPECT_TRUE(isHole);
  296. }
  297. }
  298. }
  299. // Bounds check for bounds that should and shouldn't have a terrain area inside
  300. AZ::Aabb boundsCheckCollides = spawnerBox.GetTranslated(AZ::Vector3(5.0f, 5.0f, 5.0f));
  301. EXPECT_TRUE(terrainSystem->TerrainAreaExistsInBounds(boundsCheckCollides));
  302. AZ::Aabb boundsCheckDoesNotCollide = spawnerBox.GetTranslated(AZ::Vector3(15.0f, 15.0f, 15.0f));
  303. EXPECT_FALSE(terrainSystem->TerrainAreaExistsInBounds(boundsCheckDoesNotCollide));
  304. }
  305. TEST_F(TerrainSystemTest, TerrainHeightQueriesWithExactSamplersIgnoreQueryGrid)
  306. {
  307. // Verify that when using the "EXACT" height sampler, the returned heights come directly from the height provider at the exact
  308. // requested location, instead of the position being quantized to the height query grid.
  309. // Create a mock terrain layer spawner that uses a box of (0,0,5) - (10,10,15) and generates a height based on a sine wave
  310. // using a frequency of 1m and an amplitude of 10m. i.e. Heights will range between -10 to 10 meters, but will have a value of 0
  311. // every 0.5 meters. The sine wave value is based on the absolute X position only, for simplicity.
  312. constexpr float amplitudeMeters = 10.0f;
  313. constexpr float frequencyMeters = 1.0f;
  314. const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 5.0f, 10.0f, 10.0f, 15.0f);
  315. auto entity = CreateAndActivateMockTerrainLayerSpawner(
  316. spawnerBox,
  317. [](AZ::Vector3& position, bool& terrainExists)
  318. {
  319. position.SetZ(amplitudeMeters * sin(AZ::Constants::TwoPi * (position.GetX() / frequencyMeters)));
  320. terrainExists = true;
  321. });
  322. // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution that exactly matches
  323. // the frequency of our sine wave. If our height queries rely on the query resolution, we should always get a value of 0.
  324. auto terrainSystem = CreateAndActivateTerrainSystem(frequencyMeters);
  325. // Test an arbitrary set of points that should all produce non-zero heights with the EXACT sampler. They're not aligned with the
  326. // query resolution, or with the 0 points on the sine wave.
  327. const AZ::Vector2 nonZeroPoints[] = { AZ::Vector2(0.3f), AZ::Vector2(2.8f), AZ::Vector2(5.9f), AZ::Vector2(7.7f) };
  328. for (auto& nonZeroPoint : nonZeroPoints)
  329. {
  330. AZ::Vector3 position(nonZeroPoint.GetX(), nonZeroPoint.GetY(), 0.0f);
  331. bool heightQueryTerrainExists = false;
  332. float height =
  333. terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &heightQueryTerrainExists);
  334. // We've chosen a bunch of places on the sine wave that should return a non-zero positive or negative value.
  335. constexpr float epsilon = 0.0001f;
  336. EXPECT_GT(fabsf(height), epsilon);
  337. }
  338. // Test an arbitrary set of points that should all produce zero heights with the EXACT sampler, since they align with 0 points on
  339. // the sine wave, regardless of whether or not they align to the query resolution.
  340. const AZ::Vector2 zeroPoints[] = { AZ::Vector2(0.5f), AZ::Vector2(1.0f), AZ::Vector2(5.0f), AZ::Vector2(7.5f) };
  341. for (auto& zeroPoint : zeroPoints)
  342. {
  343. AZ::Vector3 position(zeroPoint.GetX(), zeroPoint.GetY(), 0.0f);
  344. bool heightQueryTerrainExists = false;
  345. float height =
  346. terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &heightQueryTerrainExists);
  347. constexpr float epsilon = 0.0001f;
  348. EXPECT_NEAR(height, 0.0f, epsilon);
  349. }
  350. }
  351. TEST_F(TerrainSystemTest, TerrainHeightQueriesWithClampSamplersUseQueryGrid)
  352. {
  353. // Verify that when using the "CLAMP" height sampler, the requested location is quantized to the height query grid before fetching
  354. // the height.
  355. // Create a mock terrain layer spawner that uses a box of (-10,-10,-5) - (10,10,15) and generates a height equal
  356. // to the X + Y position, so if either one doesn't get clamped we'll get an unexpected result.
  357. const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f);
  358. auto entity = CreateAndActivateMockTerrainLayerSpawner(
  359. spawnerBox,
  360. [](AZ::Vector3& position, bool& terrainExists)
  361. {
  362. position.SetZ(position.GetX() + position.GetY());
  363. terrainExists = true;
  364. });
  365. // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 0.25 meter
  366. // intervals.
  367. const float queryResolution = 0.25f;
  368. auto terrainSystem = CreateAndActivateTerrainSystem(queryResolution);
  369. // Test some points and verify that the results always go "downward", whether they're in positive or negative space.
  370. // (Z contains the the expected result for convenience).
  371. const HeightTestPoint testPoints[] = {
  372. { AZ::Vector2(0.0f, 0.0f), 0.0f }, // Should return a height of 0.00 + 0.00
  373. { AZ::Vector2(0.3f, 0.3f), 0.5f }, // Should return a height of 0.25 + 0.25
  374. { AZ::Vector2(2.8f, 2.8f), 5.5f }, // Should return a height of 2.75 + 2.75
  375. { AZ::Vector2(5.5f, 5.5f), 11.0f }, // Should return a height of 5.50 + 5.50
  376. { AZ::Vector2(7.7f, 7.7f), 15.5f }, // Should return a height of 7.75 + 7.75
  377. { AZ::Vector2(-0.3f, -0.3f), -0.5f }, // Should return a height of -0.25 + -0.25
  378. { AZ::Vector2(-2.8f, -2.8f), -5.5f }, // Should return a height of -2.75 + -2.75
  379. { AZ::Vector2(-5.5f, -5.5f), -11.0f }, // Should return a height of -5.50 + -5.50
  380. { AZ::Vector2(-7.7f, -7.7f), -15.5f } // Should return a height of -7.75 + -7.75
  381. };
  382. for (auto& testPoint : testPoints)
  383. {
  384. const float expectedHeight = testPoint.m_expectedHeight;
  385. AZ::Vector3 position(testPoint.m_testLocation.GetX(), testPoint.m_testLocation.GetY(), 0.0f);
  386. bool heightQueryTerrainExists = false;
  387. float height =
  388. terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP, &heightQueryTerrainExists);
  389. constexpr float epsilon = 0.0001f;
  390. EXPECT_NEAR(height, expectedHeight, epsilon);
  391. }
  392. }
  393. TEST_F(TerrainSystemTest, TerrainHeightQueriesWithBilinearSamplersUseQueryGridToInterpolate)
  394. {
  395. // Verify that when using the "BILINEAR" height sampler, the heights are interpolated from points sampled from the query grid.
  396. // Create a mock terrain layer spawner that uses a box of (-10,-10,-5) - (10,10,15) and generates a height equal
  397. // to the X + Y position, so we'll have heights that look like this on our grid:
  398. // 0 *---* 1
  399. // | |
  400. // 1 *---* 2
  401. // However, everywhere inside the grid box, we'll generate heights much larger than X + Y. It will have no effect on exact grid
  402. // points, but it will noticeably affect the expected height values if any points get sampled in-between grid points.
  403. const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f);
  404. const float amplitudeMeters = 10.0f;
  405. const float frequencyMeters = 1.0f;
  406. auto entity = CreateAndActivateMockTerrainLayerSpawner(
  407. spawnerBox,
  408. [amplitudeMeters, frequencyMeters](AZ::Vector3& position, bool& terrainExists)
  409. {
  410. // Our generated height will be X + Y.
  411. float expectedHeight = position.GetX() + position.GetY();
  412. // If either X or Y aren't evenly divisible by the query frequency, add a scaled value to our generated height.
  413. // This will show up as an unexpected height "spike" if it gets used in any bilinear filter queries.
  414. float unexpectedVariance =
  415. amplitudeMeters * (fmodf(position.GetX(), frequencyMeters) + fmodf(position.GetY(), frequencyMeters));
  416. position.SetZ(expectedHeight + unexpectedVariance);
  417. terrainExists = true;
  418. });
  419. // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 1 meter intervals.
  420. auto terrainSystem = CreateAndActivateTerrainSystem(frequencyMeters);
  421. // Test some points and verify that the results are the expected bilinear filtered result,
  422. // whether they're in positive or negative space.
  423. // (Z contains the the expected result for convenience).
  424. const HeightTestPoint testPoints[] = {
  425. // Queries directly on grid points. These should return values of X + Y.
  426. { AZ::Vector2(0.0f, 0.0f), 0.0f }, // Should return a height of 0 + 0
  427. { AZ::Vector2(1.0f, 0.0f), 1.0f }, // Should return a height of 1 + 0
  428. { AZ::Vector2(0.0f, 1.0f), 1.0f }, // Should return a height of 0 + 1
  429. { AZ::Vector2(1.0f, 1.0f), 2.0f }, // Should return a height of 1 + 1
  430. { AZ::Vector2(3.0f, 5.0f), 8.0f }, // Should return a height of 3 + 5
  431. { AZ::Vector2(-1.0f, 0.0f), -1.0f }, // Should return a height of -1 + 0
  432. { AZ::Vector2(0.0f, -1.0f), -1.0f }, // Should return a height of 0 + -1
  433. { AZ::Vector2(-1.0f, -1.0f), -2.0f }, // Should return a height of -1 + -1
  434. { AZ::Vector2(-3.0f, -5.0f), -8.0f }, // Should return a height of -3 + -5
  435. // Queries that are on a grid edge (one axis on the grid, the other somewhere in-between).
  436. // These should just be a linear interpolation of the points, so it should still be X + Y.
  437. { AZ::Vector2(0.25f, 0.0f), 0.25f }, // Should return a height of -0.25 + 0
  438. { AZ::Vector2(3.75f, 0.0f), 3.75f }, // Should return a height of -3.75 + 0
  439. { AZ::Vector2(0.0f, 0.25f), 0.25f }, // Should return a height of 0 + -0.25
  440. { AZ::Vector2(0.0f, 3.75f), 3.75f }, // Should return a height of 0 + -3.75
  441. { AZ::Vector2(2.0f, 3.75f), 5.75f }, // Should return a height of -2 + -3.75
  442. { AZ::Vector2(2.25f, 4.0f), 6.25f }, // Should return a height of -2.25 + -4
  443. { AZ::Vector2(-0.25f, 0.0f), -0.25f }, // Should return a height of -0.25 + 0
  444. { AZ::Vector2(-3.75f, 0.0f), -3.75f }, // Should return a height of -3.75 + 0
  445. { AZ::Vector2(0.0f, -0.25f), -0.25f }, // Should return a height of 0 + -0.25
  446. { AZ::Vector2(0.0f, -3.75f), -3.75f }, // Should return a height of 0 + -3.75
  447. { AZ::Vector2(-2.0f, -3.75f), -5.75f }, // Should return a height of -2 + -3.75
  448. { AZ::Vector2(-2.25f, -4.0f), -6.25f }, // Should return a height of -2.25 + -4
  449. // Queries inside a grid square (both axes are in-between grid points)
  450. // This is a full bilinear interpolation, but because we're using X + Y for our heights, the interpolated values
  451. // should *still* be X + Y assuming the points were sampled correctly from the grid points.
  452. { AZ::Vector2(3.25f, 5.25f), 8.5f }, // Should return a height of 3.25 + 5.25
  453. { AZ::Vector2(7.71f, 8.74f), 16.45f }, // Should return a height of 7.71 + 9.74
  454. { AZ::Vector2(-3.25f, -5.25f), -8.5f }, // Should return a height of -3.25 + -5.25
  455. { AZ::Vector2(-7.71f, -8.74f), -16.45f }, // Should return a height of -7.71 + -9.74
  456. };
  457. // Loop through every test point and validate it.
  458. for (auto& testPoint : testPoints)
  459. {
  460. const float expectedHeight = testPoint.m_expectedHeight;
  461. AZ::Vector3 position(testPoint.m_testLocation.GetX(), testPoint.m_testLocation.GetY(), 0.0f);
  462. bool heightQueryTerrainExists = false;
  463. float height = terrainSystem->GetHeight(
  464. position, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR, &heightQueryTerrainExists);
  465. // Verify that our height query returned the bilinear filtered result we expect.
  466. constexpr float epsilon = 0.0001f;
  467. EXPECT_NEAR(height, expectedHeight, epsilon);
  468. EXPECT_TRUE(heightQueryTerrainExists);
  469. }
  470. }
  471. TEST_F(TerrainSystemTest, TerrainNormalQueriesWithExactSamplersUseExactHeights)
  472. {
  473. // Verify that when using the "EXACT" normal sampler, the normals are calculated from heights immediately around the query
  474. // point, instead of relying on heights that fall on the query grid.
  475. // Our height mock function will return 0 on exact grid points, and X everywhere else. If the grid points are used,
  476. // normals will get all 0 heights which will produce a Z-up normal. If the exact heights are used, the heights will
  477. // slope up and to the right at a 45 degree angle, so all the normals should point 45 degrees to the left.
  478. auto heightMockFunction = [](AZ::Vector3& position, bool& terrainExists)
  479. {
  480. terrainExists = true;
  481. if ((AZStd::fmod(position.GetX(), 1.0f) == 0.0f) && (AZStd::fmod(position.GetY(), 1.0f) == 0.0f))
  482. {
  483. // Points that fall exactly on grid squares should return 0 for height.
  484. position.SetZ(0.0f);
  485. }
  486. else
  487. {
  488. // All other points should return the X value as height.
  489. position.SetZ(position.GetX());
  490. }
  491. };
  492. // We expect the queries to never use the grid points, so all the normals should point 45 degrees to the left.
  493. const AZ::Vector3 normalLeft45Degrees =
  494. AZ::Transform::CreateRotationY(AZ::DegToRad(-45.0f)).TransformVector(AZ::Vector3::CreateAxisZ());
  495. // Get the normals for an arbitrary set of points that can either fall on or off grid points.
  496. // These should all produce normals that point 45 degrees to the left, because even for the points that fall on grid points,
  497. // the height values used for calculating the normals will fall off of the grid points.
  498. const NormalTestPoint testPoints[] = {
  499. { AZ::Vector2(0.3f), normalLeft45Degrees }, { AZ::Vector2(1.0f), normalLeft45Degrees },
  500. { AZ::Vector2(2.8f), normalLeft45Degrees }, { AZ::Vector2(3.0f), normalLeft45Degrees },
  501. { AZ::Vector2(5.9f), normalLeft45Degrees }, { AZ::Vector2(7.7f), normalLeft45Degrees },
  502. };
  503. // Test our normals
  504. TestNormals(testPoints, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, heightMockFunction);
  505. }
  506. TEST_F(TerrainSystemTest, TerrainNormalQueriesWithClampAndBilinearSamplersUseQueryGrid)
  507. {
  508. // Verify that when using the "CLAMP" or "BILINEAR" normal samplers, the normals are
  509. // calculated only from heights that fall on the query grid.
  510. // Our height mock function will return 0 on exact grid points, and X everywhere else. If the grid points are used,
  511. // normals will get all 0 heights which will produce a Z-up normal. If the exact heights are used, the heights will
  512. // slope up and to the right at a 45 degree angle, so all the normals should point 45 degrees to the left.
  513. auto heightMockFunction = [](AZ::Vector3& position, bool& terrainExists)
  514. {
  515. terrainExists = true;
  516. if ((AZStd::fmod(position.GetX(), 1.0f) == 0.0f) && (AZStd::fmod(position.GetY(), 1.0f) == 0.0f))
  517. {
  518. // Points that fall exactly on grid squares should return 0 for height.
  519. position.SetZ(0.0f);
  520. }
  521. else
  522. {
  523. // All other points should return the X value as height.
  524. position.SetZ(position.GetX());
  525. }
  526. };
  527. // We expect the queries to never use the grid points, so all the normals should point directly up.
  528. const AZ::Vector3 normalUp = AZ::Vector3::CreateAxisZ();
  529. // Get the normals for an arbitrary set of points that can either fall on or off grid points.
  530. // These should all produce normals that point directly up, because the height values used for
  531. // calculating the normals should always come from the grid points.
  532. const NormalTestPoint testPoints[] = {
  533. { AZ::Vector2(0.3f), normalUp }, { AZ::Vector2(1.0f), normalUp }, { AZ::Vector2(2.8f), normalUp },
  534. { AZ::Vector2(3.0f), normalUp }, { AZ::Vector2(5.9f), normalUp }, { AZ::Vector2(7.7f), normalUp },
  535. };
  536. // Test our normals with the CLAMP sampler, and make sure they only use the query grid
  537. TestNormals(testPoints, AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP, heightMockFunction);
  538. // Test our normals with the BILINEAR sampler, and make sure they only use the query grid
  539. TestNormals(testPoints, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR, heightMockFunction);
  540. }
  541. TEST_F(TerrainSystemTest, TerrainNormalQueriesWithClampSamplersReturnTriangleNormal)
  542. {
  543. // Verify that when using the "CLAMP" normal sampler, the returned normals are the normal of the triangle for whichever
  544. // half of the terrain grid square the query point falls in.
  545. // Our height mock function will return 1 on every other X and Y grid value so that every other bottom left triangle
  546. // will return Z-up and every other top right triangle will return a sloped normal.
  547. // We'll only pick query points from grid squares where the 1 is the upper right vertex.
  548. // 0 1 0 1 0
  549. // *--*--*--*--*
  550. // |\ |\ |\ |\ |
  551. // | \| \| \| \|
  552. // *--*--*--*--*
  553. // 0 0 0 0 0
  554. auto heightMockFunction = [](AZ::Vector3& position, bool& terrainExists)
  555. {
  556. terrainExists = true;
  557. if ((AZStd::fmod(position.GetX(), 2.0f) == 1.0f) && (AZStd::fmod(position.GetY(), 2.0f) == 1.0f))
  558. {
  559. // Grid points where X and Y are odd will get a value of 1.0
  560. // (i.e. (1,1) (3,1) (5,1) (1,3) (3,3) (5,3) etc.
  561. position.SetZ(1.0f);
  562. }
  563. else
  564. {
  565. // All other points will return 0.
  566. position.SetZ(0.0f);
  567. }
  568. };
  569. // The normals in the lower left triangle should point straight up.
  570. const AZ::Vector3 normalUp = AZ::Vector3::CreateAxisZ();
  571. // Calculate the rotated normal for the upper right triangle.
  572. const AZ::Vector3 normalRotated = (AZ::Vector3(1.0f, 1.0f, 1.0f) - AZ::Vector3(0.0f, 1.0f, 0.0f))
  573. .Cross(AZ::Vector3(1.0f, 1.0f, 1.0f) - AZ::Vector3(1.0f, 0.0f, 0.0f)).GetNormalized();
  574. const NormalTestPoint testPoints[] = {
  575. // Test points that fall in the upper right triangles.
  576. { AZ::Vector2(0.6f), normalRotated },
  577. { AZ::Vector2(2.6f, 0.8f), normalRotated },
  578. { AZ::Vector2(4.4f, 0.9f), normalRotated },
  579. // Test points that fall in the lower left triangles.
  580. { AZ::Vector2(0.3f), normalUp },
  581. { AZ::Vector2(0.5f, 0.2f), normalUp },
  582. { AZ::Vector2(2.2f, 0.7f), normalUp },
  583. { AZ::Vector2(4.4f, 0.1f), normalUp },
  584. };
  585. // Test our normals
  586. TestNormals(testPoints, AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP, heightMockFunction);
  587. }
  588. TEST_F(TerrainSystemTest, TerrainNormalQueriesWithBilinearSamplersReturnPredictableNormalsAtGridPoints)
  589. {
  590. // Verify that when using the "BILINEAR" normal sampler, if we query directly on a grid point,
  591. // we get back a predictable normal for that grid point without needing to consider interpolation.
  592. // Our height mock will return +X for every even X query point, and -X for every odd X query point.
  593. // When calculating a normal from heights gathered from a + shape, the normals will always point
  594. // 45 degrees left when the center is at an odd X value (because the two heights will come from even X values),
  595. // and they will always point 45 degrees right when the center is at an even X value.
  596. auto heightMockFunction = [](AZ::Vector3& position, bool& terrainExists)
  597. {
  598. terrainExists = true;
  599. if (AZStd::fmod(position.GetX(), 2.0f) == 0.0f)
  600. {
  601. // Return +X for even X points: (0,0), (0,1), (0,2), (2,0), (2,1), (2,2), etc.
  602. position.SetZ(position.GetX());
  603. }
  604. else
  605. {
  606. // Return -X for odd X points: (1,0), (1,1), (1,2), (3,0), (3,1), (3,2), etc.
  607. position.SetZ(-position.GetX());
  608. }
  609. };
  610. // The normals should either point left or right 45 degrees.
  611. const AZ::Vector3 normalLeft45Degrees =
  612. AZ::Transform::CreateRotationY(AZ::DegToRad(-45.0f)).TransformVector(AZ::Vector3::CreateAxisZ());
  613. const AZ::Vector3 normalRight45Degrees =
  614. AZ::Transform::CreateRotationY(AZ::DegToRad(45.0f)).TransformVector(AZ::Vector3::CreateAxisZ());
  615. const NormalTestPoint testPoints[] = {
  616. // Test points centered on odd X grid points should point left
  617. { AZ::Vector2(1.0f, 2.0f), normalLeft45Degrees },
  618. { AZ::Vector2(3.0f, 0.0f), normalLeft45Degrees },
  619. { AZ::Vector2(7.0f, 5.0f), normalLeft45Degrees },
  620. // Test points centered on even X grid points should point right
  621. { AZ::Vector2(2.0f, 2.0f), normalRight45Degrees },
  622. { AZ::Vector2(4.0f, 0.0f), normalRight45Degrees },
  623. { AZ::Vector2(8.0f, 5.0f), normalRight45Degrees },
  624. };
  625. // Test our normals
  626. TestNormals(testPoints, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR, heightMockFunction);
  627. }
  628. TEST_F(TerrainSystemTest, TerrainNormalQueriesWithBilinearSamplersReturnInterpolatedNormalsBetweenGridPoints)
  629. {
  630. // Verify that when using the "BILINEAR" normal sampler, if we query in-between grid points,
  631. // we get back a normal that is interpolated between the 4 normals on the corners of that grid square.
  632. // To test this, we'll set up a 3 x 3 grid of specific heights, and perform all our queries in the center square.
  633. // * --- 0 --- 4 --- * (3,3)
  634. //
  635. // 2 --- 2 --- 2 --- 2
  636. // x
  637. // 0 --- 2 --- 2 --- 0
  638. //
  639. // (0,0) * --- 2 --- 2 --- *
  640. // This pattern of heights should give us the following normals at each corner
  641. // X -45 deg X +45 deg Y -45 deg Y +45 deg
  642. // normal0 normal1 normal2 normal3
  643. // 2 2 0 4
  644. // | | | |
  645. // 0---*---2 2---*---0 2---*---2 2---*---2
  646. // | | | |
  647. // 2 2 2 2
  648. auto heightMockFunction = [](AZ::Vector3& position, bool& terrainExists)
  649. {
  650. const float heights[4][4] = {
  651. { 1.0f, 0.0f, 4.0f, 1.0f },
  652. { 2.0f, 2.0f, 2.0f, 2.0f },
  653. { 0.0f, 2.0f, 2.0f, 0.0f },
  654. { 1.0f, 2.0f, 2.0f, 1.0f }
  655. };
  656. terrainExists = true;
  657. uint32_t xIndex = static_cast<uint32_t>(AZStd::fmod(position.GetX(), 4.0f));
  658. uint32_t yIndex = static_cast<uint32_t>(AZStd::fmod(position.GetY(), 4.0f));
  659. // We use "3 - y" here so that we can list the heights above in the same order as the picture.
  660. position.SetZ(heights[3 - yIndex][xIndex]);
  661. };
  662. // Create and activate the terrain system with some reasonable defaults for world bounds and query resolutions.
  663. constexpr float queryResolution = 1.0f;
  664. const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -20.0f, 10.0f, 10.0f, 20.0f);
  665. auto entity = CreateAndActivateMockTerrainLayerSpawner(spawnerBox, heightMockFunction);
  666. auto terrainSystem = CreateAndActivateTerrainSystem(queryResolution);
  667. // Expected corner normals
  668. const AZStd::array<AZ::Vector3, 4> expectedCornerNormals =
  669. {
  670. AZ::Transform::CreateRotationY(AZ::DegToRad(-45.0f)).TransformVector(AZ::Vector3::CreateAxisZ()),
  671. AZ::Transform::CreateRotationY(AZ::DegToRad(+45.0f)).TransformVector(AZ::Vector3::CreateAxisZ()),
  672. AZ::Transform::CreateRotationX(AZ::DegToRad(-45.0f)).TransformVector(AZ::Vector3::CreateAxisZ()),
  673. AZ::Transform::CreateRotationX(AZ::DegToRad(+45.0f)).TransformVector(AZ::Vector3::CreateAxisZ())
  674. };
  675. // Get the normals at the four corners and verify that they match expectations.
  676. const AZStd::array<AZ::Vector3, 4> cornerPositions = {
  677. AZ::Vector3(1.0f, 1.0f, 0.0f), AZ::Vector3(2.0f, 1.0f, 0.0f), AZ::Vector3(1.0f, 2.0f, 0.0f), AZ::Vector3(2.0f, 2.0f, 0.0f)
  678. };
  679. AZStd::array<AZ::Vector3, 4> cornerNormals;
  680. AZStd::array<bool, 4> cornerExists;
  681. for (size_t corner = 0; corner < 4; corner++)
  682. {
  683. cornerNormals[corner] = terrainSystem->GetNormal(
  684. cornerPositions[corner], AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR, &(cornerExists[corner]));
  685. EXPECT_TRUE(cornerExists[corner]);
  686. EXPECT_THAT(cornerNormals[corner], UnitTest::IsClose(expectedCornerNormals[corner]));
  687. }
  688. // Now query a set of points across the terrain grid box and verify that they interpolate correctly.
  689. for (float y = 0.0f; y <= 1.0f; y += 0.125f)
  690. {
  691. for (float x = 0.0f; x <= 1.0f; x += 0.125f)
  692. {
  693. AZ::Vector3 queryPoint = cornerPositions[0] + AZ::Vector3(x, y, 0.0f);
  694. bool terrainExists = false;
  695. AZ::Vector3 normal = terrainSystem->GetNormal(
  696. queryPoint, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR, &terrainExists);
  697. const AZ::Vector3 normalLerpX0 = cornerNormals[0].Lerp(cornerNormals[1], x);
  698. const AZ::Vector3 normalLerpX1 = cornerNormals[2].Lerp(cornerNormals[3], x);
  699. const AZ::Vector3 expectedNormal = normalLerpX0.Lerp(normalLerpX1, y).GetNormalized();
  700. EXPECT_TRUE(terrainExists);
  701. EXPECT_THAT(normal, UnitTest::IsClose(expectedNormal));
  702. }
  703. }
  704. }
  705. TEST_F(TerrainSystemTest, GetSurfaceWeightsReturnsAllValidSurfaceWeightsInOrder)
  706. {
  707. // When there is more than one surface/weight defined, they should all be returned in descending weight order.
  708. auto terrainSystem = CreateAndActivateTerrainSystem();
  709. const AZ::Aabb aabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3(0.0f), AZ::Vector3(2.0f));
  710. auto entity = CreateAndActivateMockTerrainLayerSpawner(
  711. aabb,
  712. [](AZ::Vector3& position, bool& terrainExists)
  713. {
  714. position.SetZ(1.0f);
  715. terrainExists = true;
  716. });
  717. const AZ::Crc32 tag1("tag1");
  718. const AZ::Crc32 tag2("tag2");
  719. const AZ::Crc32 tag3("tag3");
  720. const float tag1Weight = 0.8f;
  721. const float tag2Weight = 1.0f;
  722. const float tag3Weight = 0.5f;
  723. AzFramework::SurfaceData::SurfaceTagWeightList orderedSurfaceWeights
  724. {
  725. { tag1, tag1Weight }, { tag2, tag2Weight }, { tag3, tag3Weight }
  726. };
  727. NiceMock<UnitTest::MockTerrainAreaSurfaceRequestBus> mockSurfaceRequests(entity->GetId());
  728. ON_CALL(mockSurfaceRequests, GetSurfaceWeights).WillByDefault(SetArgReferee<1>(orderedSurfaceWeights));
  729. AzFramework::SurfaceData::SurfaceTagWeightList outSurfaceWeights;
  730. // Asking for values outside the layer spawner bounds, should result in no results.
  731. terrainSystem->GetSurfaceWeights(aabb.GetMax() + AZ::Vector3::CreateOne(), outSurfaceWeights);
  732. EXPECT_TRUE(outSurfaceWeights.empty());
  733. // Inside the layer spawner box should give us all of the added surface weights.
  734. terrainSystem->GetSurfaceWeights(aabb.GetCenter(), outSurfaceWeights);
  735. EXPECT_EQ(outSurfaceWeights.size(), 3);
  736. // The weights should be returned in decreasing order.
  737. AZ::Crc32 expectedCrcList[] = { tag2, tag1, tag3 };
  738. const float expectedWeightList[] = { tag2Weight, tag1Weight, tag3Weight };
  739. int index = 0;
  740. for (const auto& surfaceWeight : outSurfaceWeights)
  741. {
  742. EXPECT_EQ(surfaceWeight.m_surfaceType, expectedCrcList[index]);
  743. EXPECT_NEAR(surfaceWeight.m_weight, expectedWeightList[index], 0.01f);
  744. index++;
  745. }
  746. }
  747. TEST_F(TerrainSystemTest, GetMaxSurfaceWeightsReturnsBiggestValidSurfaceWeight)
  748. {
  749. auto terrainSystem = CreateAndActivateTerrainSystem();
  750. const AZ::Aabb aabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3(0.0f), AZ::Vector3(2.0f));
  751. auto entity = CreateAndActivateMockTerrainLayerSpawner(
  752. aabb,
  753. [](AZ::Vector3& position, bool& terrainExists)
  754. {
  755. position.SetZ(1.0f);
  756. terrainExists = true;
  757. });
  758. const AZ::Crc32 tag1("tag1");
  759. const AZ::Crc32 tag2("tag2");
  760. AzFramework::SurfaceData::SurfaceTagWeightList orderedSurfaceWeights;
  761. AzFramework::SurfaceData::SurfaceTagWeight tagWeight1;
  762. tagWeight1.m_surfaceType = tag1;
  763. tagWeight1.m_weight = 1.0f;
  764. orderedSurfaceWeights.emplace_back(tagWeight1);
  765. AzFramework::SurfaceData::SurfaceTagWeight tagWeight2;
  766. tagWeight2.m_surfaceType = tag2;
  767. tagWeight2.m_weight = 0.8f;
  768. orderedSurfaceWeights.emplace_back(tagWeight2);
  769. NiceMock<UnitTest::MockTerrainAreaSurfaceRequestBus> mockSurfaceRequests(entity->GetId());
  770. ON_CALL(mockSurfaceRequests, GetSurfaceWeights).WillByDefault(SetArgReferee<1>(orderedSurfaceWeights));
  771. // Asking for values outside the layer spawner bounds, should result in an invalid result.
  772. AzFramework::SurfaceData::SurfaceTagWeight tagWeight =
  773. terrainSystem->GetMaxSurfaceWeight(aabb.GetMax() + AZ::Vector3::CreateOne());
  774. EXPECT_EQ(tagWeight.m_surfaceType, AZ::Crc32(AzFramework::SurfaceData::Constants::UnassignedTagName));
  775. // Inside the layer spawner box should give us the highest weighted tag (tag1).
  776. tagWeight = terrainSystem->GetMaxSurfaceWeight(aabb.GetCenter());
  777. EXPECT_EQ(tagWeight.m_surfaceType, tagWeight1.m_surfaceType);
  778. EXPECT_NEAR(tagWeight.m_weight, tagWeight1.m_weight, 0.01f);
  779. }
  780. TEST_F(TerrainSystemTest, GetSurfacePointAndIndividualQueriesProduceSameResults)
  781. {
  782. // Verify that the height / normal / surface weights returned from GetSurfacePoint matches the results
  783. // that we get from individually querying GetHeight, GetNormal, and GetSurfaceWeights.
  784. // We don't need to validate all combinations because we have separate unit tests that validate equivalent results between
  785. // the different variations of each individual API. The transitive property means that if those are equal and the results here
  786. // are equal, then all the combinations will be equal as well.
  787. // Set up the arbitrary terrain world parameters that we'll use for verifying our queries match.
  788. const float terrainSize = 32.0f;
  789. const float terrainQueryResolution = 1.0f;
  790. const uint32_t terrainNumSurfaces = 3;
  791. const AZ::Aabb terrainWorldBounds =
  792. AZ::Aabb::CreateFromMinMax(AZ::Vector3(-terrainSize / 2.0f), AZ::Vector3(terrainSize / 2.0f));
  793. // Set up the query bounds and step size to use for selecting the points to query and compare.
  794. const AZ::Aabb queryBounds = terrainWorldBounds;
  795. const AZ::Vector2 queryStepSize = AZ::Vector2(terrainQueryResolution / 2.0f);
  796. CreateTestTerrainSystem(terrainWorldBounds, terrainQueryResolution, terrainNumSurfaces);
  797. for (auto sampler : { AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR,
  798. AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP,
  799. AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT })
  800. {
  801. for (float y = queryBounds.GetMin().GetY(); y < queryBounds.GetMax().GetY(); y += queryStepSize.GetY())
  802. {
  803. for (float x = queryBounds.GetMin().GetX(); y < queryBounds.GetMax().GetX(); y += queryStepSize.GetX())
  804. {
  805. AZ::Vector3 queryPosition(x, y, 0.0f);
  806. // GetHeight
  807. float expectedHeight = terrainWorldBounds.GetMin().GetZ();
  808. bool heightExists = false;
  809. AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
  810. expectedHeight, &AzFramework::Terrain::TerrainDataRequests::GetHeight, queryPosition, sampler, &heightExists);
  811. // GetNormal
  812. AZ::Vector3 expectedNormal = AZ::Vector3::CreateAxisZ();
  813. bool normalExists = false;
  814. AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
  815. expectedNormal, &AzFramework::Terrain::TerrainDataRequests::GetNormal, queryPosition, sampler, &normalExists);
  816. // GetSurfaceWeights
  817. AzFramework::SurfaceData::SurfaceTagWeightList expectedWeights;
  818. bool weightsExist = false;
  819. AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
  820. &AzFramework::Terrain::TerrainDataRequests::GetSurfaceWeights,
  821. queryPosition, expectedWeights, sampler, &weightsExist);
  822. // GetSurfacePoint
  823. AzFramework::SurfaceData::SurfacePoint surfacePoint;
  824. bool pointExists = false;
  825. AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
  826. &AzFramework::Terrain::TerrainDataRequests::GetSurfacePoint, queryPosition, surfacePoint, sampler, &pointExists);
  827. // Verify that all the results match.
  828. EXPECT_EQ(heightExists, pointExists);
  829. EXPECT_EQ(expectedHeight, surfacePoint.m_position.GetZ());
  830. EXPECT_THAT(expectedNormal, UnitTest::IsClose(surfacePoint.m_normal));
  831. EXPECT_EQ(expectedWeights, surfacePoint.m_surfaceTags);
  832. }
  833. }
  834. }
  835. DestroyTestTerrainSystem();
  836. }
  837. TEST_F(TerrainSystemTest, TerrainProcessHeightsFromListWithBilinearSamplers)
  838. {
  839. // This repeats the same test as TerrainHeightQueriesWithBilinearSamplersUseQueryGridToInterpolate
  840. // The difference is that it tests the ProcessHeightsFromList variation.
  841. const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f);
  842. const float amplitudeMeters = 10.0f;
  843. const float frequencyMeters = 1.0f;
  844. auto entity = CreateAndActivateMockTerrainLayerSpawner(
  845. spawnerBox,
  846. [amplitudeMeters, frequencyMeters](AZ::Vector3& position, bool& terrainExists)
  847. {
  848. // Our generated height will be X + Y.
  849. float expectedHeight = position.GetX() + position.GetY();
  850. // If either X or Y aren't evenly divisible by the query frequency, add a scaled value to our generated height.
  851. // This will show up as an unexpected height "spike" if it gets used in any bilinear filter queries.
  852. float unexpectedVariance =
  853. amplitudeMeters * (fmodf(position.GetX(), frequencyMeters) + fmodf(position.GetY(), frequencyMeters));
  854. position.SetZ(expectedHeight + unexpectedVariance);
  855. terrainExists = true;
  856. });
  857. // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 1 meter intervals.
  858. auto terrainSystem = CreateAndActivateTerrainSystem(frequencyMeters);
  859. // Test some points and verify that the results are the expected bilinear filtered result,
  860. // whether they're in positive or negative space.
  861. // (Z contains the the expected result for convenience).
  862. const HeightTestPoint testPoints[] = {
  863. // Queries directly on grid points. These should return values of X + Y.
  864. { AZ::Vector2(0.0f, 0.0f), 0.0f }, // Should return a height of 0 + 0
  865. { AZ::Vector2(1.0f, 0.0f), 1.0f }, // Should return a height of 1 + 0
  866. { AZ::Vector2(0.0f, 1.0f), 1.0f }, // Should return a height of 0 + 1
  867. { AZ::Vector2(1.0f, 1.0f), 2.0f }, // Should return a height of 1 + 1
  868. { AZ::Vector2(3.0f, 5.0f), 8.0f }, // Should return a height of 3 + 5
  869. { AZ::Vector2(-1.0f, 0.0f), -1.0f }, // Should return a height of -1 + 0
  870. { AZ::Vector2(0.0f, -1.0f), -1.0f }, // Should return a height of 0 + -1
  871. { AZ::Vector2(-1.0f, -1.0f), -2.0f }, // Should return a height of -1 + -1
  872. { AZ::Vector2(-3.0f, -5.0f), -8.0f }, // Should return a height of -3 + -5
  873. // Queries that are on a grid edge (one axis on the grid, the other somewhere in-between).
  874. // These should just be a linear interpolation of the points, so it should still be X + Y.
  875. { AZ::Vector2(0.25f, 0.0f), 0.25f }, // Should return a height of -0.25 + 0
  876. { AZ::Vector2(3.75f, 0.0f), 3.75f }, // Should return a height of -3.75 + 0
  877. { AZ::Vector2(0.0f, 0.25f), 0.25f }, // Should return a height of 0 + -0.25
  878. { AZ::Vector2(0.0f, 3.75f), 3.75f }, // Should return a height of 0 + -3.75
  879. { AZ::Vector2(2.0f, 3.75f), 5.75f }, // Should return a height of -2 + -3.75
  880. { AZ::Vector2(2.25f, 4.0f), 6.25f }, // Should return a height of -2.25 + -4
  881. { AZ::Vector2(-0.25f, 0.0f), -0.25f }, // Should return a height of -0.25 + 0
  882. { AZ::Vector2(-3.75f, 0.0f), -3.75f }, // Should return a height of -3.75 + 0
  883. { AZ::Vector2(0.0f, -0.25f), -0.25f }, // Should return a height of 0 + -0.25
  884. { AZ::Vector2(0.0f, -3.75f), -3.75f }, // Should return a height of 0 + -3.75
  885. { AZ::Vector2(-2.0f, -3.75f), -5.75f }, // Should return a height of -2 + -3.75
  886. { AZ::Vector2(-2.25f, -4.0f), -6.25f }, // Should return a height of -2.25 + -4
  887. // Queries inside a grid square (both axes are in-between grid points)
  888. // This is a full bilinear interpolation, but because we're using X + Y for our heights, the interpolated values
  889. // should *still* be X + Y assuming the points were sampled correctly from the grid points.
  890. { AZ::Vector2(3.25f, 5.25f), 8.5f }, // Should return a height of 3.25 + 5.25
  891. { AZ::Vector2(7.71f, 8.74f), 16.45f }, // Should return a height of 7.71 + 8.74
  892. // We don't test any points > 9.0f because our AABB is max-exclusive, and would query grid points that don't exist for use as
  893. // a part of the interpolation. We'll test those cases separately, as they're more complex.
  894. { AZ::Vector2(-3.25f, -5.25f), -8.5f }, // Should return a height of -3.25 + -5.25
  895. { AZ::Vector2(-7.71f, -9.74f), -17.45f }, // Should return a height of -7.71 + -9.74
  896. };
  897. auto perPositionCallback = [&testPoints](const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists){
  898. bool found = false;
  899. for (auto& testPoint : testPoints)
  900. {
  901. if (testPoint.m_testLocation.GetX() == surfacePoint.m_position.GetX() && testPoint.m_testLocation.GetY() == surfacePoint.m_position.GetY())
  902. {
  903. constexpr float epsilon = 0.0001f;
  904. EXPECT_NEAR(surfacePoint.m_position.GetZ(), testPoint.m_expectedHeight, epsilon);
  905. found = true;
  906. break;
  907. }
  908. }
  909. EXPECT_EQ(found, true);
  910. };
  911. AZStd::vector<AZ::Vector3> inPositions;
  912. for (auto& testPoint : testPoints)
  913. {
  914. AZ::Vector3 position(testPoint.m_testLocation.GetX(), testPoint.m_testLocation.GetY(), 0.0f);
  915. inPositions.push_back(position);
  916. }
  917. terrainSystem->QueryList(
  918. inPositions, AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Heights, perPositionCallback,
  919. AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR);
  920. }
  921. TEST_F(TerrainSystemTest, TerrainProcessNormalsFromListWithBilinearSamplers)
  922. {
  923. // Similar to TerrainProcessHeightsFromListWithBilinearSamplers but for normals
  924. const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f);
  925. const float amplitudeMeters = 10.0f;
  926. const float frequencyMeters = 1.0f;
  927. auto entity = CreateAndActivateMockTerrainLayerSpawner(
  928. spawnerBox,
  929. [amplitudeMeters, frequencyMeters](AZ::Vector3& position, bool& terrainExists)
  930. {
  931. // Our generated height will be X + Y.
  932. float expectedHeight = position.GetX() + position.GetY();
  933. // If either X or Y aren't evenly divisible by the query frequency, add a scaled value to our generated height.
  934. // This will show up as an unexpected height "spike" if it gets used in any bilinear filter queries.
  935. float unexpectedVariance =
  936. amplitudeMeters * (fmodf(position.GetX(), frequencyMeters) + fmodf(position.GetY(), frequencyMeters));
  937. position.SetZ(expectedHeight + unexpectedVariance);
  938. terrainExists = true;
  939. });
  940. // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 1 meter intervals.
  941. auto terrainSystem = CreateAndActivateTerrainSystem(frequencyMeters);
  942. // Note that we keep our test points in the range -9.5 to +8.5. Any value outside that range would use points that don't exist
  943. // in the calculation of the normals, which is more complex and can get tested separately.
  944. const NormalTestPoint testPoints[] = {
  945. { AZ::Vector2(0.0f, 0.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) },
  946. { AZ::Vector2(1.0f, 0.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) },
  947. { AZ::Vector2(0.0f, 1.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) },
  948. { AZ::Vector2(1.0f, 1.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) },
  949. { AZ::Vector2(3.0f, 5.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) },
  950. { AZ::Vector2(-1.0f, 0.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) },
  951. { AZ::Vector2(0.0f, -1.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) },
  952. { AZ::Vector2(-1.0f, -1.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) },
  953. { AZ::Vector2(-3.0f, -5.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) },
  954. { AZ::Vector2(0.25f, 0.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) },
  955. { AZ::Vector2(3.75f, 0.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) },
  956. { AZ::Vector2(0.0f, 0.25f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) },
  957. { AZ::Vector2(0.0f, 3.75f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) },
  958. { AZ::Vector2(2.0f, 3.75f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) },
  959. { AZ::Vector2(2.25f, 4.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) },
  960. { AZ::Vector2(-0.25f, 0.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) },
  961. { AZ::Vector2(-3.75f, 0.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) },
  962. { AZ::Vector2(0.0f, -0.25f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) },
  963. { AZ::Vector2(0.0f, -3.75f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) },
  964. { AZ::Vector2(-2.0f, -3.75f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) },
  965. { AZ::Vector2(-2.25f, -4.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) },
  966. { AZ::Vector2(3.25f, 5.25f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) },
  967. { AZ::Vector2(7.71f, 7.74f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) },
  968. { AZ::Vector2(-3.25f, -5.25f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) },
  969. { AZ::Vector2(-7.71f, -7.74f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) },
  970. };
  971. auto perPositionCallback = [&testPoints](const AzFramework::SurfaceData::SurfacePoint& surfacePoint, bool terrainExists){
  972. bool found = false;
  973. for (auto& testPoint : testPoints)
  974. {
  975. if (testPoint.m_testLocation.GetX() == surfacePoint.m_position.GetX() && testPoint.m_testLocation.GetY() == surfacePoint.m_position.GetY())
  976. {
  977. constexpr float epsilon = 0.0001f;
  978. EXPECT_NEAR(surfacePoint.m_normal.GetX(), testPoint.m_expectedNormal.GetX(), epsilon);
  979. EXPECT_NEAR(surfacePoint.m_normal.GetY(), testPoint.m_expectedNormal.GetY(), epsilon);
  980. EXPECT_NEAR(surfacePoint.m_normal.GetZ(), testPoint.m_expectedNormal.GetZ(), epsilon);
  981. EXPECT_TRUE(terrainExists);
  982. found = true;
  983. break;
  984. }
  985. }
  986. EXPECT_EQ(found, true);
  987. };
  988. AZStd::vector<AZ::Vector3> inPositions;
  989. for (auto& testPoint : testPoints)
  990. {
  991. AZ::Vector3 position(testPoint.m_testLocation.GetX(), testPoint.m_testLocation.GetY(), 0.0f);
  992. inPositions.push_back(position);
  993. }
  994. terrainSystem->QueryList(
  995. inPositions, AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Normals, perPositionCallback,
  996. AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR);
  997. }
  998. TEST_F(TerrainSystemTest, TerrainProcessHeightsFromRegionWithBilinearSamplers)
  999. {
  1000. // This repeats the same test as TerrainHeightQueriesWithBilinearSamplersUseQueryGridToInterpolate
  1001. // The difference is that it tests the ProcessHeightsFromList variation.
  1002. const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f);
  1003. const float amplitudeMeters = 10.0f;
  1004. const float frequencyMeters = 1.0f;
  1005. auto entity = CreateAndActivateMockTerrainLayerSpawner(
  1006. spawnerBox,
  1007. [amplitudeMeters, frequencyMeters](AZ::Vector3& position, bool& terrainExists)
  1008. {
  1009. // Our generated height will be X + Y.
  1010. float expectedHeight = position.GetX() + position.GetY();
  1011. // If either X or Y aren't evenly divisible by the query frequency, add a scaled value to our generated height.
  1012. // This will show up as an unexpected height "spike" if it gets used in any bilinear filter queries.
  1013. float unexpectedVariance =
  1014. amplitudeMeters * (fmodf(position.GetX(), frequencyMeters) + fmodf(position.GetY(), frequencyMeters));
  1015. position.SetZ(expectedHeight + unexpectedVariance);
  1016. terrainExists = true;
  1017. });
  1018. // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 1 meter intervals.
  1019. auto terrainSystem = CreateAndActivateTerrainSystem(frequencyMeters);
  1020. // Set up a query region that starts at (-1, -1, -1), queries 2 points in the X and Y direction, and uses a step size of 1.0.
  1021. // This should query (-1, -1), (0, -1), (-1, 0), and (0, 0).
  1022. const AZ::Vector2 stepSize(1.0f);
  1023. const AzFramework::Terrain::TerrainQueryRegion queryRegion(AZ::Vector3(-1.0f), 2, 2, stepSize);
  1024. const HeightTestRegionPoints testPoints[] = {
  1025. { 0, 0, -2.0f, AZ::Vector2(-1.0f, -1.0f) },
  1026. { 1, 0, -1.0f, AZ::Vector2(0.0f, -1.0f) },
  1027. { 0, 1, -1.0f, AZ::Vector2(-1.0f, 0.0f) },
  1028. { 1, 1, 0.0f, AZ::Vector2(0.0f, 0.0f) },
  1029. };
  1030. auto perPositionCallback = [&testPoints](size_t xIndex, size_t yIndex,
  1031. const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists)
  1032. {
  1033. bool found = false;
  1034. for (auto& testPoint : testPoints)
  1035. {
  1036. if (testPoint.m_xIndex == xIndex && testPoint.m_yIndex == yIndex
  1037. && testPoint.m_testLocation.GetX() == surfacePoint.m_position.GetX()
  1038. && testPoint.m_testLocation.GetY() == surfacePoint.m_position.GetY())
  1039. {
  1040. constexpr float epsilon = 0.0001f;
  1041. EXPECT_NEAR(surfacePoint.m_position.GetZ(), testPoint.m_expectedHeight, epsilon);
  1042. found = true;
  1043. break;
  1044. }
  1045. }
  1046. EXPECT_EQ(found, true);
  1047. };
  1048. terrainSystem->QueryRegion(
  1049. queryRegion, AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Heights, perPositionCallback,
  1050. AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR);
  1051. }
  1052. TEST_F(TerrainSystemTest, TerrainProcessNormalsFromRegionWithBilinearSamplers)
  1053. {
  1054. // This repeats the same test as TerrainHeightQueriesWithBilinearSamplersUseQueryGridToInterpolate
  1055. // The difference is that it tests the ProcessHeightsFromList variation.
  1056. const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f);
  1057. const float amplitudeMeters = 10.0f;
  1058. const float frequencyMeters = 1.0f;
  1059. auto entity = CreateAndActivateMockTerrainLayerSpawner(
  1060. spawnerBox,
  1061. [amplitudeMeters, frequencyMeters](AZ::Vector3& position, bool& terrainExists)
  1062. {
  1063. // Our generated height will be X + Y.
  1064. float expectedHeight = position.GetX() + position.GetY();
  1065. // If either X or Y aren't evenly divisible by the query frequency, add a scaled value to our generated height.
  1066. // This will show up as an unexpected height "spike" if it gets used in any bilinear filter queries.
  1067. float unexpectedVariance =
  1068. amplitudeMeters * (fmodf(position.GetX(), frequencyMeters) + fmodf(position.GetY(), frequencyMeters));
  1069. position.SetZ(expectedHeight + unexpectedVariance);
  1070. terrainExists = true;
  1071. });
  1072. // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 1 meter intervals.
  1073. auto terrainSystem = CreateAndActivateTerrainSystem(frequencyMeters);
  1074. // Set up a query region that starts at (-1, -1, -1), queries 2 points in the X and Y direction, and uses a step size of 1.0.
  1075. // This should query (-1, -1), (0, -1), (-1, 0), and (0, 0).
  1076. const AZ::Vector2 stepSize(1.0f);
  1077. const AzFramework::Terrain::TerrainQueryRegion queryRegion(AZ::Vector3(-1.0f), 2, 2, stepSize);
  1078. const NormalTestRegionPoints testPoints[] = {
  1079. { 0, 0, AZ::Vector3(-0.5773f, -0.5773f, 0.5773f), AZ::Vector2(-1.0f, -1.0f) },
  1080. { 1, 0, AZ::Vector3(-0.5773f, -0.5773f, 0.5773f), AZ::Vector2(0.0f, -1.0f) },
  1081. { 0, 1, AZ::Vector3(-0.5773f, -0.5773f, 0.5773f), AZ::Vector2(-1.0f, 0.0f) },
  1082. { 1, 1, AZ::Vector3(-0.5773f, -0.5773f, 0.5773f), AZ::Vector2(0.0f, 0.0f) },
  1083. };
  1084. auto perPositionCallback = [&testPoints](size_t xIndex, size_t yIndex,
  1085. const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists)
  1086. {
  1087. bool found = false;
  1088. for (auto& testPoint : testPoints)
  1089. {
  1090. if (testPoint.m_xIndex == xIndex && testPoint.m_yIndex == yIndex
  1091. && testPoint.m_testLocation.GetX() == surfacePoint.m_position.GetX()
  1092. && testPoint.m_testLocation.GetY() == surfacePoint.m_position.GetY())
  1093. {
  1094. constexpr float epsilon = 0.0001f;
  1095. EXPECT_NEAR(surfacePoint.m_normal.GetX(), testPoint.m_expectedNormal.GetX(), epsilon);
  1096. EXPECT_NEAR(surfacePoint.m_normal.GetY(), testPoint.m_expectedNormal.GetY(), epsilon);
  1097. EXPECT_NEAR(surfacePoint.m_normal.GetZ(), testPoint.m_expectedNormal.GetZ(), epsilon);
  1098. found = true;
  1099. break;
  1100. }
  1101. }
  1102. EXPECT_EQ(found, true);
  1103. };
  1104. terrainSystem->QueryRegion(
  1105. queryRegion, AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Normals, perPositionCallback,
  1106. AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR);
  1107. }
  1108. TEST_F(TerrainSystemTest, TerrainProcessSurfaceWeightsFromRegion)
  1109. {
  1110. const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f);
  1111. auto entity = CreateAndActivateMockTerrainLayerSpawner(
  1112. spawnerBox,
  1113. [](AZ::Vector3& position, bool& terrainExists)
  1114. {
  1115. position.SetZ(1.0f);
  1116. terrainExists = true;
  1117. });
  1118. // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 1 meter intervals.
  1119. const float queryResolution = 1.0f;
  1120. auto terrainSystem = CreateAndActivateTerrainSystem(queryResolution);
  1121. // Set up a query region that starts at (-3, -3, -1), queries 6 points in the X and Y direction, and uses a step size of 1.0.
  1122. const AZ::Vector2 stepSize(1.0f);
  1123. const AzFramework::Terrain::TerrainQueryRegion queryRegion(AZ::Vector3(-3.0f, -3.0f, -1.0f), 6, 6, stepSize);
  1124. AzFramework::SurfaceData::SurfaceTagWeightList expectedTags;
  1125. SetupSurfaceWeightMocks(entity.get(), expectedTags);
  1126. auto perPositionCallback = [&expectedTags]([[maybe_unused]] size_t xIndex, [[maybe_unused]] size_t yIndex,
  1127. const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists)
  1128. {
  1129. constexpr float epsilon = 0.0001f;
  1130. float absYPos = fabsf(surfacePoint.m_position.GetY());
  1131. if (absYPos < 1.0f)
  1132. {
  1133. EXPECT_EQ(surfacePoint.m_surfaceTags[0].m_surfaceType, expectedTags[0].m_surfaceType);
  1134. EXPECT_NEAR(surfacePoint.m_surfaceTags[0].m_weight, expectedTags[0].m_weight, epsilon);
  1135. }
  1136. else if(absYPos < 2.0f)
  1137. {
  1138. EXPECT_EQ(surfacePoint.m_surfaceTags[0].m_surfaceType, expectedTags[1].m_surfaceType);
  1139. EXPECT_NEAR(surfacePoint.m_surfaceTags[0].m_weight, expectedTags[1].m_weight, epsilon);
  1140. }
  1141. else
  1142. {
  1143. EXPECT_EQ(surfacePoint.m_surfaceTags[0].m_surfaceType, expectedTags[2].m_surfaceType);
  1144. EXPECT_NEAR(surfacePoint.m_surfaceTags[0].m_weight, expectedTags[2].m_weight, epsilon);
  1145. }
  1146. };
  1147. terrainSystem->QueryRegion(
  1148. queryRegion, AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::SurfaceData, perPositionCallback,
  1149. AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR);
  1150. }
  1151. TEST_F(TerrainSystemTest, TerrainProcessSurfacePointsFromRegion)
  1152. {
  1153. const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f);
  1154. auto entity = CreateAndActivateMockTerrainLayerSpawner(
  1155. spawnerBox,
  1156. [](AZ::Vector3& position, bool& terrainExists)
  1157. {
  1158. position.SetZ(position.GetX() + position.GetY());
  1159. terrainExists = true;
  1160. });
  1161. // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 1 meter intervals.
  1162. const float queryResolution = 1.0f;
  1163. auto terrainSystem = CreateAndActivateTerrainSystem(queryResolution);
  1164. // Set up a query region that starts at (-3, -3, -1), queries 6 points in the X and Y direction, and uses a step size of 1.0.
  1165. const AZ::Vector2 stepSize(1.0f);
  1166. const AzFramework::Terrain::TerrainQueryRegion queryRegion(AZ::Vector3(-3.0f, -3.0f, -1.0f), 6, 6, stepSize);
  1167. AzFramework::SurfaceData::SurfaceTagWeightList expectedTags;
  1168. SetupSurfaceWeightMocks(entity.get(), expectedTags);
  1169. auto perPositionCallback = [&expectedTags]([[maybe_unused]] size_t xIndex, [[maybe_unused]] size_t yIndex,
  1170. const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists)
  1171. {
  1172. constexpr float epsilon = 0.0001f;
  1173. float expectedHeight = surfacePoint.m_position.GetX() + surfacePoint.m_position.GetY();
  1174. EXPECT_NEAR(surfacePoint.m_position.GetZ(), expectedHeight, epsilon);
  1175. float absYPos = fabsf(surfacePoint.m_position.GetY());
  1176. if (absYPos < 1.0f)
  1177. {
  1178. EXPECT_EQ(surfacePoint.m_surfaceTags[0].m_surfaceType, expectedTags[0].m_surfaceType);
  1179. EXPECT_NEAR(surfacePoint.m_surfaceTags[0].m_weight, expectedTags[0].m_weight, epsilon);
  1180. }
  1181. else if(absYPos < 2.0f)
  1182. {
  1183. EXPECT_EQ(surfacePoint.m_surfaceTags[0].m_surfaceType, expectedTags[1].m_surfaceType);
  1184. EXPECT_NEAR(surfacePoint.m_surfaceTags[0].m_weight, expectedTags[1].m_weight, epsilon);
  1185. }
  1186. else
  1187. {
  1188. EXPECT_EQ(surfacePoint.m_surfaceTags[0].m_surfaceType, expectedTags[2].m_surfaceType);
  1189. EXPECT_NEAR(surfacePoint.m_surfaceTags[0].m_weight, expectedTags[2].m_weight, epsilon);
  1190. }
  1191. };
  1192. terrainSystem->QueryRegion(
  1193. queryRegion, AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::All, perPositionCallback,
  1194. AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT);
  1195. }
  1196. TEST_F(TerrainSystemTest, TerrainGetClosestIntersection)
  1197. {
  1198. // Create a Terrain Spawner with a box from (-200, -200, 0) to (200, 200, 50) that always returns a height of 0.
  1199. // We intentionally match the bottom of the box with the returned height so that the terrain intersection tests need
  1200. // to match intersections that occur on the box surface.
  1201. const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-200.0f, -200.0f, 0.0f, 200.0f, 200.0f, 50.0f);
  1202. auto entity = CreateAndActivateMockTerrainLayerSpawner(
  1203. spawnerBox,
  1204. [](AZ::Vector3& position, bool& terrainExists)
  1205. {
  1206. position.SetZ(0.0f);
  1207. terrainExists = true;
  1208. });
  1209. // Create a random number generator in the -100 to 100 range. We'll use this to generate XY coordinates that
  1210. // always exist within the Terrain Spawner XY dimensions. These are guaranteed to cause an intersection with
  1211. // the terrain as long as we use a +Z value for the start coordinate and a -Z value for the end.
  1212. constexpr unsigned int Seed = 1;
  1213. std::mt19937_64 rng(Seed);
  1214. std::uniform_real_distribution<float> unif(-100.0f, 100.0f);
  1215. // We'll track the total number of intersections failures so that we have a quick reference number to look at if we
  1216. // get spammed with failures.
  1217. int32_t numFailures = 0;
  1218. // Run through a variety of query resolutions to ensure that changing it doesn't break the intersection tests.
  1219. for (float queryResolution : { 0.13f, 0.25f, 0.5f, 1.0f, 2.0f, 3.0f })
  1220. {
  1221. // Create and activate the terrain system.
  1222. auto terrainSystem = CreateAndActivateTerrainSystem(queryResolution);
  1223. // Run through an arbitrary number of random rays and ensure that they all collide with the terrain.
  1224. constexpr uint32_t NumRays = 100;
  1225. for (uint32_t test = 0; test < NumRays; test++)
  1226. {
  1227. // Generate a ray with random XY values in the -100 to 100 range,
  1228. // but with a start Z of 1 to 101 and an end Z of -1 to -101 so that we're guaranteed an intersection for every ray.
  1229. AzFramework::RenderGeometry::RayRequest ray;
  1230. ray.m_startWorldPosition = AZ::Vector3(unif(rng), unif(rng), abs(unif(rng)) + 1.0f);
  1231. ray.m_endWorldPosition = AZ::Vector3(unif(rng), unif(rng), -abs(unif(rng)) - 1.0f);
  1232. // Get our intersection.
  1233. auto result = terrainSystem->GetClosestIntersection(ray);
  1234. // Every ray should intersect at a height of 0 and a normal pointing directly up.
  1235. EXPECT_TRUE(result);
  1236. EXPECT_NEAR(result.m_worldPosition.GetZ(), 0.0f, 0.001f);
  1237. EXPECT_THAT(result.m_worldNormal, IsClose(AZ::Vector3::CreateAxisZ()));
  1238. // Track any intersection failures
  1239. numFailures += (result ? 0 : 1);
  1240. }
  1241. }
  1242. // This is here just to give us a final tally of how many rays failed this test.
  1243. EXPECT_EQ(numFailures, 0);
  1244. }
  1245. TEST_F(TerrainSystemTest, TerrainProcessAsyncCancellation)
  1246. {
  1247. // Tests cancellation of the asynchronous terrain API.
  1248. const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f);
  1249. auto entity = CreateAndActivateMockTerrainLayerSpawner(
  1250. spawnerBox,
  1251. [](AZ::Vector3& position, bool& terrainExists)
  1252. {
  1253. // Our generated height will be X + Y.
  1254. position.SetZ(position.GetX() + position.GetY());
  1255. terrainExists = true;
  1256. });
  1257. // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 1 meter intervals.
  1258. auto terrainSystem = CreateAndActivateTerrainSystem();
  1259. // Generate some input positions.
  1260. AZStd::vector<AZ::Vector3> inPositions;
  1261. for (int i = 0; i < 16; ++i)
  1262. {
  1263. inPositions.push_back({1.0f, 1.0f, 1.0f});
  1264. }
  1265. // Setup the per position callback so that we can cancel the entire request when it is first invoked.
  1266. AZStd::atomic_bool asyncRequestCancelled = false;
  1267. AZStd::binary_semaphore asyncRequestStartedEvent;
  1268. AZStd::binary_semaphore asyncRequestCancelledEvent;
  1269. auto perPositionCallback = [&asyncRequestCancelled, &asyncRequestStartedEvent, &asyncRequestCancelledEvent]([[maybe_unused]] const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists)
  1270. {
  1271. if (!asyncRequestCancelled)
  1272. {
  1273. // Indicate that the async request has started.
  1274. asyncRequestStartedEvent.release();
  1275. // Wait until the async request has been cancelled before allowing it to continue.
  1276. asyncRequestCancelledEvent.acquire();
  1277. asyncRequestCancelled = true;
  1278. }
  1279. };
  1280. // Setup the completion callback so we can check that the entire request was cancelled.
  1281. AZStd::semaphore asyncRequestCompletedEvent;
  1282. auto completionCallback = [&asyncRequestCompletedEvent](AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> terrainJobContext)
  1283. {
  1284. EXPECT_TRUE(terrainJobContext->IsCancelled());
  1285. asyncRequestCompletedEvent.release();
  1286. };
  1287. // Invoke the async request.
  1288. AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> asyncParams
  1289. = AZStd::make_shared<AzFramework::Terrain::QueryAsyncParams>();
  1290. // Only use one job. We're using a lot of handshaking logic to ensure we process the main thread test logic and the callback logic
  1291. // in the exact order we want for the test, and this logic assumes only one job is running.
  1292. asyncParams->m_desiredNumberOfJobs = 1;
  1293. asyncParams->m_completionCallback = completionCallback;
  1294. AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> terrainJobContext = terrainSystem->QueryListAsync(
  1295. inPositions, AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Heights, perPositionCallback,
  1296. AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR, asyncParams);
  1297. // Wait until the async request has started before cancelling it.
  1298. asyncRequestStartedEvent.acquire();
  1299. terrainJobContext->Cancel();
  1300. asyncRequestCancelled = true;
  1301. asyncRequestCancelledEvent.release();
  1302. // Now wait until the async request has completed after being cancelled.
  1303. asyncRequestCompletedEvent.acquire();
  1304. }
  1305. } // namespace UnitTest