3
0

TerrainSystem.cpp 82 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829
  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 <TerrainSystem/TerrainSystem.h>
  9. #include <AzCore/std/parallel/shared_mutex.h>
  10. #include <AzCore/std/sort.h>
  11. #include <SurfaceData/SurfaceDataTypes.h>
  12. #include <SurfaceData/SurfaceDataSystemRequestBus.h>
  13. #include <SurfaceData/Utility/SurfaceDataUtility.h>
  14. #include <LmbrCentral/Shape/ShapeComponentBus.h>
  15. #include <Atom/RPI.Public/Scene.h>
  16. #include <Atom/RPI.Public/FeatureProcessorFactory.h>
  17. #include <TerrainRenderer/TerrainFeatureProcessor.h>
  18. #include <Terrain/Ebuses/TerrainAreaSurfaceRequestBus.h>
  19. #include <TerrainProfiler.h>
  20. using namespace Terrain;
  21. AZ_DEFINE_BUDGET(Terrain);
  22. bool TerrainLayerPriorityComparator::operator()(const AZ::EntityId& layer1id, const AZ::EntityId& layer2id) const
  23. {
  24. // Comparator for insertion/key lookup.
  25. // Sorts into layer/priority order, highest priority first.
  26. int32_t priority1 = 0;
  27. uint32_t layer1 = 0;
  28. Terrain::TerrainSpawnerRequestBus::Event(layer1id, &Terrain::TerrainSpawnerRequestBus::Events::GetPriority, layer1, priority1);
  29. int32_t priority2 = 0;
  30. uint32_t layer2 = 0;
  31. Terrain::TerrainSpawnerRequestBus::Event(layer2id, &Terrain::TerrainSpawnerRequestBus::Events::GetPriority, layer2, priority2);
  32. if (layer1 < layer2)
  33. {
  34. return false;
  35. }
  36. else if (layer1 > layer2)
  37. {
  38. return true;
  39. }
  40. if (priority1 != priority2)
  41. {
  42. return priority1 > priority2;
  43. }
  44. return layer1id > layer2id;
  45. }
  46. TerrainSystem::TerrainSystem()
  47. : m_terrainRaycastContext(*this)
  48. {
  49. Terrain::TerrainSystemServiceRequestBus::Handler::BusConnect();
  50. AZ::TickBus::Handler::BusConnect();
  51. m_currentSettings.m_systemActive = false;
  52. m_currentSettings.m_heightRange = AzFramework::Terrain::FloatRange::CreateNull();
  53. m_requestedSettings = m_currentSettings;
  54. m_requestedSettings.m_heightRange = { -512.0f, 512.0f };
  55. // Use the global JobManager for terrain jobs (we could create our own dedicated terrain JobManager if needed).
  56. AZ::JobManagerBus::BroadcastResult(m_terrainJobManager, &AZ::JobManagerEvents::GetManager);
  57. AZ_Assert(m_terrainJobManager, "No global JobManager found.");
  58. }
  59. TerrainSystem::~TerrainSystem()
  60. {
  61. AZ::TickBus::Handler::BusDisconnect();
  62. Terrain::TerrainSystemServiceRequestBus::Handler::BusDisconnect();
  63. Deactivate();
  64. }
  65. void TerrainSystem::Activate()
  66. {
  67. AzFramework::Terrain::TerrainDataNotificationBus::Broadcast(
  68. &AzFramework::Terrain::TerrainDataNotificationBus::Events::OnTerrainDataCreateBegin);
  69. m_dirtyRegion = AZ::Aabb::CreateNull();
  70. m_terrainDirtyMask = AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask::All;
  71. m_requestedSettings.m_systemActive = true;
  72. m_cachedAreaBounds = AZ::Aabb::CreateNull();
  73. {
  74. AZStd::unique_lock<AZStd::shared_mutex> lock(m_areaMutex);
  75. m_registeredAreas.clear();
  76. }
  77. AzFramework::Terrain::TerrainDataRequestBus::Handler::BusConnect();
  78. // Register any terrain spawners that were already active before the terrain system activated.
  79. auto enumerationCallback = [&]([[maybe_unused]] Terrain::TerrainSpawnerRequests* terrainSpawner) -> bool
  80. {
  81. AZ::EntityId areaId = *(Terrain::TerrainSpawnerRequestBus::GetCurrentBusId());
  82. RegisterArea(areaId);
  83. // Keep Enumerating
  84. return true;
  85. };
  86. Terrain::TerrainSpawnerRequestBus::EnumerateHandlers(enumerationCallback);
  87. AzFramework::Terrain::TerrainDataNotificationBus::Broadcast(
  88. &AzFramework::Terrain::TerrainDataNotificationBus::Events::OnTerrainDataCreateEnd);
  89. }
  90. void TerrainSystem::Deactivate()
  91. {
  92. {
  93. // Cancel all active terrain jobs, and wait until they have completed.
  94. AZStd::unique_lock<AZStd::mutex> lock(m_activeTerrainJobContextMutex);
  95. for (auto activeTerrainJobContext : m_activeTerrainJobContexts)
  96. {
  97. activeTerrainJobContext->Cancel();
  98. }
  99. m_activeTerrainJobContextMutexConditionVariable.wait(lock, [this]{ return m_activeTerrainJobContexts.empty(); });
  100. }
  101. // Stop listening to the bus even before we signal DestroyBegin so that way any calls to the terrain system as a *result* of
  102. // calling DestroyBegin will fail to reach the terrain system.
  103. AzFramework::Terrain::TerrainDataRequestBus::Handler::BusDisconnect();
  104. AzFramework::Terrain::TerrainDataNotificationBus::Broadcast(
  105. &AzFramework::Terrain::TerrainDataNotificationBus::Events::OnTerrainDataDestroyBegin);
  106. {
  107. AZStd::unique_lock<AZStd::shared_mutex> lock(m_areaMutex);
  108. m_registeredAreas.clear();
  109. }
  110. m_dirtyRegion = AZ::Aabb::CreateNull();
  111. m_terrainDirtyMask = AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask::All;
  112. m_requestedSettings.m_systemActive = false;
  113. AzFramework::Terrain::TerrainDataNotificationBus::Broadcast(
  114. &AzFramework::Terrain::TerrainDataNotificationBus::Events::OnTerrainDataDestroyEnd);
  115. }
  116. void TerrainSystem::SetTerrainHeightBounds(const AzFramework::Terrain::FloatRange& heightRange)
  117. {
  118. m_requestedSettings.m_heightRange = heightRange;
  119. m_terrainDirtyMask |= AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask::Settings;
  120. }
  121. bool TerrainSystem::TerrainAreaExistsInBounds(const AZ::Aabb& bounds) const
  122. {
  123. for (const auto& area : m_registeredAreas)
  124. {
  125. if (area.second.m_areaBounds.Overlaps(bounds))
  126. {
  127. return true;
  128. }
  129. }
  130. return false;
  131. }
  132. void TerrainSystem::SetTerrainHeightQueryResolution(float queryResolution)
  133. {
  134. m_requestedSettings.m_heightQueryResolution = queryResolution;
  135. m_terrainDirtyMask |= AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask::Settings;
  136. }
  137. void TerrainSystem::SetTerrainSurfaceDataQueryResolution(float queryResolution)
  138. {
  139. m_requestedSettings.m_surfaceDataQueryResolution = queryResolution;
  140. m_terrainDirtyMask |= AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask::Settings;
  141. }
  142. AZ::Aabb TerrainSystem::GetTerrainAabb() const
  143. {
  144. return ClampZBoundsToHeightBounds(m_cachedAreaBounds);
  145. }
  146. AzFramework::Terrain::FloatRange TerrainSystem::GetTerrainHeightBounds() const
  147. {
  148. return m_currentSettings.m_heightRange;
  149. }
  150. float TerrainSystem::GetTerrainHeightQueryResolution() const
  151. {
  152. return m_currentSettings.m_heightQueryResolution;
  153. }
  154. float TerrainSystem::GetTerrainSurfaceDataQueryResolution() const
  155. {
  156. return m_currentSettings.m_surfaceDataQueryResolution;
  157. }
  158. void TerrainSystem::ClampPosition(float x, float y, float queryResolution, AZ::Vector2& outPosition, AZ::Vector2& normalizedDelta)
  159. {
  160. // Given an input position, clamp the values to our terrain grid, where it will always go to the terrain grid point
  161. // at a lower value, whether positive or negative. Ex: 3.3 -> 3, -3.3 -> -4
  162. // Also, return the normalized delta as a value of [0-1) describing what fraction of a grid point the value moved.
  163. // Scale the position by the query resolution, so that integer values represent exact steps on the grid,
  164. // and fractional values are the amount in-between each grid point, in the range [0-1).
  165. AZ::Vector2 normalizedPosition = AZ::Vector2(x, y) / queryResolution;
  166. normalizedDelta = AZ::Vector2(
  167. normalizedPosition.GetX() - floor(normalizedPosition.GetX()), normalizedPosition.GetY() - floor(normalizedPosition.GetY()));
  168. // Remove the fractional part, then scale back down into world space.
  169. outPosition = (normalizedPosition - normalizedDelta) * queryResolution;
  170. }
  171. void TerrainSystem::RoundPosition(float x, float y, float queryResolution, AZ::Vector2& outPosition)
  172. {
  173. // Given an input position, clamp the values to our terrain grid, where it will always go to the nearest terrain grid point
  174. // whether positive or negative. Ex: 3.3 -> 3, 3.6 -> 4, -3.3 -> -3, -3.6 -> -4
  175. // Scale the position by the query resolution, so that integer values represent exact steps on the grid,
  176. // and fractional values are the amount in-between each grid point, in the range [0-1).
  177. AZ::Vector2 normalizedPosition = AZ::Vector2(x, y) / queryResolution;
  178. // Round the fractional part, then scale back down into world space.
  179. // Note that we use "floor(pos + 0.5f)" instead of round() because round() will round to the nearest even integer (banker's rounding)
  180. // on the 0.5 points instead of to the nearest integer biased away from 0 (symmetric arithmetic rounding), which is what we want.
  181. // "floor(pos + 0.5f)" will round 1.5 -> 2, 2.5 -> 3, -1.5 -> -2, -2.5 -> -3, etc.
  182. // (i.e. don't use: outPosition = normalizedPosition.GetRound() * queryResolution;)
  183. outPosition = (normalizedPosition + AZ::Vector2(0.5f)).GetFloor() * queryResolution;
  184. }
  185. void TerrainSystem::InterpolateHeights(const AZStd::array<float,4>& heights, const AZStd::array<bool,4>& exists,
  186. float lerpX, float lerpY, float& outHeight, bool& outExists)
  187. {
  188. // When interpolating between 4 height points, we also need to take the existence of the 4 points into account.
  189. // The logic below uses a precomputed lookup table to determine how to interpolate the points in each combination of existence.
  190. // The final "terrain exists" flag gets computed based on the existence of the corner that's closest to the interpolated point.
  191. uint8_t indexLookup = (exists[3] << 3) | (exists[2] << 2) | (exists[1] << 1) | (exists[0] << 0);
  192. constexpr uint8_t heightIndices[16][4] =
  193. {
  194. // x0y0 x1y0 x0y1 x1y1 output
  195. { 0, 0, 0, 0 }, // F F F F x0y0
  196. { 0, 0, 0, 0 }, // T F F F x0y0
  197. { 1, 1, 1, 1 }, // F T F F x1y0
  198. { 0, 1, 0, 1 }, // T T F F lerp(x0y0, x1y0)
  199. { 2, 2, 2, 2 }, // F F T F x0y1
  200. { 0, 0, 2, 2 }, // T F T F lerp(x0y0, x0y1)
  201. { 1, 1, 2, 2 }, // F T T F lerp(x1y0, x0y1)
  202. { 0, 1, 2, 2 }, // T T T F lerp(lerp(x0y0, x1y0), x0y1)
  203. { 3, 3, 3, 3 }, // F F F T x1y1
  204. { 0, 0, 3, 3 }, // T F F T lerp(x0y0, x1y1)
  205. { 1, 1, 3, 3 }, // F T F T lerp(x1y0, x1y1)
  206. { 0, 1, 3, 3 }, // T T F T lerp(lerp(x0y0, x1y0), x1y1)
  207. { 2, 3, 2, 3 }, // F F T T lerp(x0y1, x1y1)
  208. { 0, 0, 2, 3 }, // T F T T lerp(x0y0, lerp(x0y1, x1y1))
  209. { 1, 1, 2, 2 }, // F T T T lerp(x1y0, lerp(x0y1, x1y1))
  210. { 0, 1, 2, 3 }, // T T T T lerp(lerp(x0y0, x1y0), lerp(x0y1, x1y1))
  211. };
  212. const float heightX0Y0 = heights[heightIndices[indexLookup][0]];
  213. const float heightX1Y0 = heights[heightIndices[indexLookup][1]];
  214. const float heightX0Y1 = heights[heightIndices[indexLookup][2]];
  215. const float heightX1Y1 = heights[heightIndices[indexLookup][3]];
  216. const float heightXY0 = AZ::Lerp(heightX0Y0, heightX1Y0, lerpX);
  217. const float heightXY1 = AZ::Lerp(heightX0Y1, heightX1Y1, lerpX);
  218. outHeight = AZ::Lerp(heightXY0, heightXY1, lerpY);
  219. // "Terrain exists" is set based on the existance of the nearest vertex to the point,
  220. // which is determined by which 1/4 of the quad the point falls in. We can determine that based on
  221. // which side of 0.5 our lerp X and Y values land on.
  222. uint8_t existsIndex = ((lerpY >= 0.5f) << 1) | (lerpX >= 0.5f);
  223. outExists = exists[existsIndex];
  224. }
  225. void TerrainSystem::RecalculateCachedBounds()
  226. {
  227. m_cachedAreaBounds = AZ::Aabb::CreateNull();
  228. for (const auto& [entityid, area] : m_registeredAreas)
  229. {
  230. m_cachedAreaBounds.AddAabb(area.m_areaBounds);
  231. }
  232. }
  233. AZ::Aabb TerrainSystem::ClampZBoundsToHeightBounds(const AZ::Aabb& aabb) const
  234. {
  235. if (!aabb.IsValid())
  236. {
  237. return aabb; // Don't try to clamp invalid aabbs
  238. }
  239. AZ::Vector3 min = aabb.GetMin();
  240. AZ::Vector3 max = aabb.GetMax();
  241. min.SetZ(AZ::GetClamp<float>(min.GetZ(), m_currentSettings.m_heightRange.m_min, m_currentSettings.m_heightRange.m_max));
  242. max.SetZ(AZ::GetClamp<float>(max.GetZ(), m_currentSettings.m_heightRange.m_min, m_currentSettings.m_heightRange.m_max));
  243. return AZ::Aabb::CreateFromMinMax(min, max);
  244. }
  245. // Generate positions to be queried based on the sampler type.
  246. void TerrainSystem::GenerateQueryPositions(const AZStd::span<const AZ::Vector3>& inPositions,
  247. AZStd::vector<AZ::Vector3>& outPositions, float queryResolution,
  248. Sampler sampler) const
  249. {
  250. TERRAIN_PROFILE_FUNCTION_VERBOSE
  251. const float minHeight = m_currentSettings.m_heightRange.m_min;
  252. for (auto& position : inPositions)
  253. {
  254. switch(sampler)
  255. {
  256. case AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR:
  257. {
  258. AZ::Vector2 normalizedDelta;
  259. AZ::Vector2 pos0;
  260. ClampPosition(position.GetX(), position.GetY(), queryResolution, pos0, normalizedDelta);
  261. const AZ::Vector2 pos1(pos0.GetX() + queryResolution, pos0.GetY() + queryResolution);
  262. outPositions.emplace_back(AZ::Vector3(pos0.GetX(), pos0.GetY(), minHeight));
  263. outPositions.emplace_back(AZ::Vector3(pos1.GetX(), pos0.GetY(), minHeight));
  264. outPositions.emplace_back(AZ::Vector3(pos0.GetX(), pos1.GetY(), minHeight));
  265. outPositions.emplace_back(AZ::Vector3(pos1.GetX(), pos1.GetY(), minHeight));
  266. }
  267. break;
  268. case AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP:
  269. {
  270. AZ::Vector2 clampedPosition;
  271. RoundPosition(position.GetX(), position.GetY(), queryResolution, clampedPosition);
  272. outPositions.emplace_back(AZ::Vector3(clampedPosition.GetX(), clampedPosition.GetY(), minHeight));
  273. }
  274. break;
  275. case AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT:
  276. [[fallthrough]];
  277. default:
  278. outPositions.emplace_back(AZ::Vector3(position.GetX(), position.GetY(), minHeight));
  279. break;
  280. }
  281. }
  282. }
  283. AZStd::vector<AZ::Vector3> TerrainSystem::GenerateInputPositionsFromRegion(
  284. const AzFramework::Terrain::TerrainQueryRegion& queryRegion) const
  285. {
  286. AZ_PROFILE_FUNCTION(Terrain);
  287. AZStd::vector<AZ::Vector3> inPositions;
  288. inPositions.reserve(queryRegion.m_numPointsX * queryRegion.m_numPointsY);
  289. AZ::Vector2 startPosition(queryRegion.m_startPoint);
  290. for (size_t y = 0; y < queryRegion.m_numPointsY; y++)
  291. {
  292. float fy = aznumeric_cast<float>(queryRegion.m_startPoint.GetY() + (y * queryRegion.m_stepSize.GetY()));
  293. for (size_t x = 0; x < queryRegion.m_numPointsX; x++)
  294. {
  295. float fx = aznumeric_cast<float>(queryRegion.m_startPoint.GetX() + (x * queryRegion.m_stepSize.GetX()));
  296. inPositions.emplace_back(AZ::Vector3(fx, fy, 0.0f));
  297. }
  298. }
  299. return inPositions;
  300. }
  301. AZStd::vector<AZ::Vector3> TerrainSystem::GenerateInputPositionsFromListOfVector2(
  302. const AZStd::span<const AZ::Vector2> inPositionsVec2) const
  303. {
  304. AZ_PROFILE_FUNCTION(Terrain);
  305. AZStd::vector<AZ::Vector3> inPositions;
  306. inPositions.reserve(inPositionsVec2.size());
  307. for (auto& pos : inPositionsVec2)
  308. {
  309. inPositions.emplace_back(AZ::Vector3(pos.GetX(), pos.GetY(), 0.0f));
  310. }
  311. return inPositions;
  312. }
  313. void TerrainSystem::MakeBulkQueries(
  314. const AZStd::span<const AZ::Vector3> inPositions,
  315. AZStd::span<AZ::Vector3> outPositions,
  316. AZStd::span<bool> outTerrainExists,
  317. AZStd::span<AzFramework::SurfaceData::SurfaceTagWeightList> outSurfaceWeights,
  318. BulkQueriesCallback queryCallback) const
  319. {
  320. TERRAIN_PROFILE_FUNCTION_VERBOSE
  321. AZStd::shared_lock<AZStd::shared_mutex> lock(m_areaMutex);
  322. AZ::Aabb bounds;
  323. // We use a sliding window here and update the window end for each
  324. // position that falls in the same area as the previous positions. This consumes lesser memory
  325. // than sorting the points into separate lists and handling putting them back together.
  326. // This may be sub optimal if the points are randomly distributed in the list as opposed
  327. // to points in the same area id being close to each other.
  328. size_t windowStart = 0;
  329. AZ::EntityId windowAreaId = FindBestAreaEntityAtPosition(inPositions[0], bounds);
  330. const size_t inPositionSize = inPositions.size();
  331. for (size_t windowEnd = 0; windowEnd < inPositionSize; windowEnd++)
  332. {
  333. size_t nextWindowEnd = windowEnd + 1;
  334. AZ::EntityId areaId = (nextWindowEnd < inPositionSize)
  335. ? FindBestAreaEntityAtPosition(inPositions[nextWindowEnd], bounds)
  336. : AZ::EntityId();
  337. if (areaId != windowAreaId)
  338. {
  339. // If the area id is a default entity id, it usually means the
  340. // position is outside world bounds.
  341. if (windowAreaId != AZ::EntityId())
  342. {
  343. size_t spanLength = (windowEnd - windowStart) + 1;
  344. queryCallback(
  345. AZStd::span<const AZ::Vector3>(inPositions.begin() + windowStart, spanLength),
  346. AZStd::span<AZ::Vector3>(outPositions.begin() + windowStart, spanLength),
  347. AZStd::span<bool>(outTerrainExists.begin() + windowStart, spanLength),
  348. AZStd::span<AzFramework::SurfaceData::SurfaceTagWeightList>(outSurfaceWeights.begin() + windowStart, spanLength),
  349. windowAreaId);
  350. }
  351. // Reset the window to start at the current position. Set the new area
  352. // id on which to run the next query.
  353. windowStart = nextWindowEnd;
  354. windowAreaId = areaId;
  355. }
  356. }
  357. }
  358. void TerrainSystem::GetHeightsSynchronous(const AZStd::span<const AZ::Vector3>& inPositions, Sampler sampler,
  359. AZStd::span<float> heights, AZStd::span<bool> terrainExists) const
  360. {
  361. TERRAIN_PROFILE_FUNCTION_VERBOSE
  362. AZStd::shared_lock<AZStd::shared_mutex> lock(m_areaMutex);
  363. AZStd::vector<AZ::Vector3> outPositions;
  364. AZStd::vector<bool> outTerrainExists;
  365. // outPositions holds the iterators to results of the bulk queries.
  366. // In the case of the bilinear sampler, we'll be making 4 queries per
  367. // input position.
  368. size_t indexStepSize = (sampler == AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) ? 4 : 1;
  369. outPositions.reserve(inPositions.size() * indexStepSize);
  370. outTerrainExists.resize(inPositions.size() * indexStepSize);
  371. const float queryResolution = m_currentSettings.m_heightQueryResolution;
  372. GenerateQueryPositions(inPositions, outPositions, queryResolution, sampler);
  373. auto callback = [this]([[maybe_unused]] const AZStd::span<const AZ::Vector3> inPositions,
  374. AZStd::span<AZ::Vector3> outPositions,
  375. AZStd::span<bool> outTerrainExists,
  376. [[maybe_unused]] AZStd::span<AzFramework::SurfaceData::SurfaceTagWeightList> outSurfaceWeights,
  377. AZ::EntityId areaId)
  378. {
  379. AZ_Assert((inPositions.size() == outPositions.size() && inPositions.size() == outTerrainExists.size()),
  380. "The sizes of the terrain exists list and in/out positions list should match.");
  381. Terrain::TerrainAreaHeightRequestBus::Event(areaId, &Terrain::TerrainAreaHeightRequestBus::Events::GetHeights,
  382. outPositions, outTerrainExists);
  383. // If the area has "use ground plane" checked, make sure any points that fall in the area that didn't
  384. // return data are filled in with the area's minimum height.
  385. const auto& area = m_registeredAreas.find(areaId);
  386. if ((area != m_registeredAreas.end()) && area->second.m_useGroundPlane)
  387. {
  388. const float areaMin = AZStd::clamp(
  389. area->second.m_areaBounds.GetMin().GetZ(),
  390. m_currentSettings.m_heightRange.m_min,
  391. m_currentSettings.m_heightRange.m_max);
  392. for (size_t index = 0; index < outPositions.size(); index++)
  393. {
  394. if (!outTerrainExists[index])
  395. {
  396. outTerrainExists[index] = true;
  397. outPositions[index].SetZ(areaMin);
  398. }
  399. }
  400. }
  401. };
  402. // This will be unused for heights. It's fine if it's empty.
  403. AZStd::vector<AzFramework::SurfaceData::SurfaceTagWeightList> outSurfaceWeights;
  404. MakeBulkQueries(outPositions, outPositions, outTerrainExists, outSurfaceWeights, callback);
  405. // Compute/store the final result
  406. for (size_t i = 0, iteratorIndex = 0; i < inPositions.size(); i++, iteratorIndex += indexStepSize)
  407. {
  408. switch(sampler)
  409. {
  410. case AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR:
  411. {
  412. // We now need to compute the final height after all the bulk queries are done.
  413. AZ::Vector2 normalizedDelta;
  414. AZ::Vector2 clampedPosition;
  415. ClampPosition(inPositions[i].GetX(), inPositions[i].GetY(), queryResolution, clampedPosition, normalizedDelta);
  416. AZStd::array<float,4> queriedHeights = { outPositions[iteratorIndex].GetZ(),
  417. outPositions[iteratorIndex + 1].GetZ(),
  418. outPositions[iteratorIndex + 2].GetZ(),
  419. outPositions[iteratorIndex + 3].GetZ() };
  420. AZStd::array<bool, 4> queriedExistsFlags = { outTerrainExists[iteratorIndex],
  421. outTerrainExists[iteratorIndex + 1],
  422. outTerrainExists[iteratorIndex + 2],
  423. outTerrainExists[iteratorIndex + 3]};
  424. InterpolateHeights(queriedHeights, queriedExistsFlags,
  425. normalizedDelta.GetX(), normalizedDelta.GetY(), heights[i], terrainExists[i]);
  426. }
  427. break;
  428. case AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP:
  429. [[fallthrough]];
  430. case AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT:
  431. [[fallthrough]];
  432. default:
  433. // For clamp and exact, we just need to store the results of the bulk query.
  434. heights[i] = outPositions[iteratorIndex].GetZ();
  435. terrainExists[i] = outTerrainExists[iteratorIndex];
  436. break;
  437. }
  438. }
  439. }
  440. float TerrainSystem::GetHeightSynchronous(float x, float y, Sampler sampler, bool* terrainExistsPtr) const
  441. {
  442. bool terrainExists = false;
  443. AZStd::shared_lock<AZStd::shared_mutex> lock(m_areaMutex);
  444. float height = m_currentSettings.m_heightRange.m_min;
  445. const float queryResolution = m_currentSettings.m_heightQueryResolution;
  446. switch (sampler)
  447. {
  448. // Get the value at the requested location, using the terrain grid to bilinear filter between sample grid points.
  449. case AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR:
  450. {
  451. // pos0 contains one corner of our grid square, pos1 contains the opposite corner, and normalizedDelta is the fractional
  452. // amount the position exists between those corners.
  453. // Ex: (3.3, 4.4) would have a pos0 of (3, 4), a pos1 of (4, 5), and a delta of (0.3, 0.4).
  454. AZ::Vector2 normalizedDelta;
  455. AZ::Vector2 pos0;
  456. ClampPosition(x, y, queryResolution, pos0, normalizedDelta);
  457. const AZ::Vector2 pos1 = pos0 + AZ::Vector2(queryResolution);
  458. AZStd::array<bool,4> exists = { false, false, false, false };
  459. const AZStd::array<float, 4> queriedHeights = { GetTerrainAreaHeight(pos0.GetX(), pos0.GetY(), exists[0]),
  460. GetTerrainAreaHeight(pos1.GetX(), pos0.GetY(), exists[1]),
  461. GetTerrainAreaHeight(pos0.GetX(), pos1.GetY(), exists[2]),
  462. GetTerrainAreaHeight(pos1.GetX(), pos1.GetY(), exists[3]) };
  463. InterpolateHeights(queriedHeights, exists, normalizedDelta.GetX(), normalizedDelta.GetY(), height, terrainExists);
  464. }
  465. break;
  466. //! Clamp the input point to the terrain sample grid, then get the height at the given grid location.
  467. case AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP:
  468. {
  469. AZ::Vector2 clampedPosition;
  470. RoundPosition(x, y, queryResolution, clampedPosition);
  471. height = GetTerrainAreaHeight(clampedPosition.GetX(), clampedPosition.GetY(), terrainExists);
  472. }
  473. break;
  474. //! Directly get the value at the location, regardless of terrain sample grid density.
  475. case AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT:
  476. [[fallthrough]];
  477. default:
  478. height = GetTerrainAreaHeight(x, y, terrainExists);
  479. break;
  480. }
  481. // For now, always set terrainExists to true, as we don't have a way to author data for terrain holes yet.
  482. if (terrainExistsPtr)
  483. {
  484. *terrainExistsPtr = terrainExists;
  485. }
  486. return AZ::GetClamp(
  487. height, m_currentSettings.m_heightRange.m_min, m_currentSettings.m_heightRange.m_max);
  488. }
  489. float TerrainSystem::GetTerrainAreaHeight(float x, float y, bool& terrainExists) const
  490. {
  491. const float worldMin = m_currentSettings.m_heightRange.m_min;
  492. AZ::Vector3 inPosition(x, y, worldMin);
  493. float height = worldMin;
  494. terrainExists = false;
  495. AZStd::shared_lock<AZStd::shared_mutex> lock(m_areaMutex);
  496. for (auto& [areaId, areaData] : m_registeredAreas)
  497. {
  498. const float areaMin = areaData.m_areaBounds.GetMin().GetZ();
  499. inPosition.SetZ(areaMin);
  500. if (SurfaceData::AabbContains2DMaxExclusive(areaData.m_areaBounds, inPosition))
  501. {
  502. AZ::Vector3 outPosition;
  503. Terrain::TerrainAreaHeightRequestBus::Event(
  504. areaId, &Terrain::TerrainAreaHeightRequestBus::Events::GetHeight, inPosition, outPosition, terrainExists);
  505. height = outPosition.GetZ();
  506. if (!terrainExists)
  507. {
  508. // If the terrain height provider doesn't have any data, then check the area's "use ground plane" setting.
  509. // If it's set, then create a default ground plane by saying terrain exists at the minimum height for the area.
  510. // Otherwise, we'll set the height at the terrain world minimum and say it doesn't exist.
  511. terrainExists = areaData.m_useGroundPlane;
  512. height = areaData.m_useGroundPlane ? areaMin : worldMin;
  513. }
  514. break;
  515. }
  516. }
  517. return height;
  518. }
  519. float TerrainSystem::GetHeight(const AZ::Vector3& position, Sampler sampler, bool* terrainExistsPtr) const
  520. {
  521. return GetHeightSynchronous(position.GetX(), position.GetY(), sampler, terrainExistsPtr);
  522. }
  523. float TerrainSystem::GetHeightFromVector2(const AZ::Vector2& position, Sampler sampler, bool* terrainExistsPtr) const
  524. {
  525. return GetHeightSynchronous(position.GetX(), position.GetY(), sampler, terrainExistsPtr);
  526. }
  527. float TerrainSystem::GetHeightFromFloats(float x, float y, Sampler sampler, bool* terrainExistsPtr) const
  528. {
  529. return GetHeightSynchronous(x, y, sampler, terrainExistsPtr);
  530. }
  531. bool TerrainSystem::GetIsHole(const AZ::Vector3& position, Sampler sampler) const
  532. {
  533. bool terrainExists = false;
  534. GetHeightSynchronous(position.GetX(), position.GetY(), sampler, &terrainExists);
  535. return !terrainExists;
  536. }
  537. bool TerrainSystem::GetIsHoleFromVector2(const AZ::Vector2& position, Sampler sampler) const
  538. {
  539. bool terrainExists = false;
  540. GetHeightSynchronous(position.GetX(), position.GetY(), sampler, &terrainExists);
  541. return !terrainExists;
  542. }
  543. bool TerrainSystem::GetIsHoleFromFloats(float x, float y, Sampler sampler) const
  544. {
  545. bool terrainExists = false;
  546. GetHeightSynchronous(x, y, sampler, &terrainExists);
  547. return !terrainExists;
  548. }
  549. void TerrainSystem::GetNormalsSynchronous(const AZStd::span<const AZ::Vector3>& inPositions, Sampler sampler,
  550. AZStd::span<AZ::Vector3> normals, AZStd::span<bool> terrainExists) const
  551. {
  552. TERRAIN_PROFILE_FUNCTION_VERBOSE
  553. // We use different algorithms for calculating the normals depending on the input sampler type,
  554. // with no real shared logic, so they've been split out into separate methods.
  555. switch (sampler)
  556. {
  557. case Sampler::EXACT:
  558. // This will return the normal of the requested point using the underlying height data at a much higher frequency than the
  559. // query resolution, which means the normal can have significant fluctuations across each terrain grid square.
  560. GetNormalsSynchronousExact(inPositions, normals, terrainExists);
  561. break;
  562. case Sampler::CLAMP:
  563. // This will treat each terrain grid square as two triangles, and return the normal for the triangle that contains the requested
  564. // point. There is no interpolation, so each terrain grid square will exactly have two possible normals that can get returned
  565. // for any queried point within the square.
  566. GetNormalsSynchronousClamp(inPositions, normals, terrainExists);
  567. break;
  568. case Sampler::BILINEAR:
  569. // This will smoothly interpolate the normals from grid point to grid point across the entire terrain grid.
  570. GetNormalsSynchronousBilinear(inPositions, normals, terrainExists);
  571. break;
  572. default:
  573. AZ_Assert(false, "Unknown sampler type");
  574. break;
  575. }
  576. }
  577. void TerrainSystem::GetNormalsSynchronousExact(
  578. const AZStd::span<const AZ::Vector3>& inPositions,
  579. AZStd::span<AZ::Vector3> normals,
  580. AZStd::span<bool> terrainExists) const
  581. {
  582. // When querying for normals with an EXACT sampler, we get the normal by querying 4 points around the requested position
  583. // in an extremely small + pattern, then get the cross product of the two lines of the plus sign.
  584. // Because we're querying for exact heights from the underlying data, independent of the query resolution, we want to make
  585. // the + as small as possible so that we can correctly capture high-frequency changes in the data.
  586. // We've arbitrarily chosen the smaller of 1/32 of a meter or 1/32 of our query resolution as the size of each leg of the + sign.
  587. // It's possible that we may want to expose this as a tuning variable at some point.
  588. const float exactRange = AZStd::min(1.0f / 32.0f, m_currentSettings.m_heightQueryResolution / 32.0f);
  589. // The number of points that we're querying for each normal.
  590. const size_t queryCount = 4;
  591. // The full set of positions to query to be able to calculate all the normals.
  592. AZStd::vector<AZ::Vector3> queryPositions;
  593. queryPositions.reserve(inPositions.size() * queryCount);
  594. for (const auto& position : inPositions)
  595. {
  596. // For each input position, query the four outer points of the + sign.
  597. queryPositions.emplace_back(AZ::Vector3(position.GetX(), position.GetY() - exactRange, 0.0f)); // down
  598. queryPositions.emplace_back(AZ::Vector3(position.GetX() - exactRange, position.GetY(), 0.0f)); // left
  599. queryPositions.emplace_back(AZ::Vector3(position.GetX() + exactRange, position.GetY(), 0.0f)); // right
  600. queryPositions.emplace_back(AZ::Vector3(position.GetX(), position.GetY() + exactRange, 0.0f)); // up
  601. }
  602. // These constants are the relative index for each of the four positions that we pushed for each input above.
  603. constexpr size_t down = 0;
  604. constexpr size_t left = 1;
  605. constexpr size_t right = 2;
  606. constexpr size_t up = 3;
  607. AZStd::vector<float> heights(queryPositions.size());
  608. AZStd::vector<bool> exists(queryPositions.size());
  609. // We want to query the underlying heights with an EXACT sampler as well, so that we get to the input data that might be
  610. // at a much higher frequency than the height query resolution.
  611. GetHeightsSynchronous(queryPositions, Sampler::EXACT, heights, exists);
  612. for (size_t inPosIndex = 0, queryPositionIndex = 0; inPosIndex < inPositions.size(); inPosIndex++, queryPositionIndex += queryCount)
  613. {
  614. terrainExists[inPosIndex] = true;
  615. for (size_t querySubindex = 0; querySubindex < queryCount; querySubindex++)
  616. {
  617. // Combine the output heights with our query positions.
  618. queryPositions[queryPositionIndex + querySubindex].SetZ(heights[queryPositionIndex + querySubindex]);
  619. // We're querying at a much higher frequency than our query resolution, so we'll simply say that the terrain
  620. // exists only if all 4 points of the + sign exist.
  621. terrainExists[inPosIndex] = terrainExists[inPosIndex] && exists[queryPositionIndex + querySubindex];
  622. }
  623. if (terrainExists[inPosIndex])
  624. {
  625. // We have 4 vertices that make a + sign, cross the two lines to get the normal at the center.
  626. // (right - left) x (up - down)
  627. normals[inPosIndex] = (queryPositions[queryPositionIndex + right] - queryPositions[queryPositionIndex + left])
  628. .Cross(queryPositions[queryPositionIndex + up] - queryPositions[queryPositionIndex + down])
  629. .GetNormalized();
  630. }
  631. else
  632. {
  633. // If at least one of the 4 points of the + sign didn't exist, just give it a Z-up normal.
  634. normals[inPosIndex] = AZ::Vector3::CreateAxisZ();
  635. }
  636. }
  637. }
  638. void TerrainSystem::GetNormalsSynchronousClamp(
  639. const AZStd::span<const AZ::Vector3>& inPositions,
  640. AZStd::span<AZ::Vector3> normals,
  641. AZStd::span<bool> terrainExists) const
  642. {
  643. // When querying for normals with a CLAMP sampler, we divide each terrain grid square into two triangles, and return the normal
  644. // for the triangle that the requested position falls on.
  645. // Right now, the terrain system will always split each terrain grid square like this: |\|
  646. // Eventually, the terrain system might get more complicated per-square logic, at which point the logic below would need
  647. // to change to account for the per-square triangle split direction.
  648. const float queryResolution = m_currentSettings.m_heightQueryResolution;
  649. // The number of points we're querying for each normal
  650. const size_t queryCount = 3;
  651. // The full set of positions to query to be able to calculate all the normals.
  652. AZStd::vector<AZ::Vector3> queryPositions;
  653. queryPositions.reserve(inPositions.size() * queryCount);
  654. for (const auto& position : inPositions)
  655. {
  656. // For each position, determine where in the square the point falls and get the bottom left corner of the square.
  657. AZ::Vector2 normalizedDelta;
  658. AZ::Vector2 bottomLeft;
  659. ClampPosition(position.GetX(), position.GetY(), queryResolution, bottomLeft, normalizedDelta);
  660. // Calculate the four corners of our grid square:
  661. // 2 *-* 3
  662. // |\|
  663. // 0 *-* 1
  664. AZStd::array<AZ::Vector3, 4> corners =
  665. {
  666. AZ::Vector3(bottomLeft.GetX() , bottomLeft.GetY(), 0.0f),
  667. AZ::Vector3(bottomLeft.GetX() + queryResolution, bottomLeft.GetY(), 0.0f),
  668. AZ::Vector3(bottomLeft.GetX() , bottomLeft.GetY() + queryResolution, 0.0f),
  669. AZ::Vector3(bottomLeft.GetX() + queryResolution, bottomLeft.GetY() + queryResolution, 0.0f)
  670. };
  671. // Our grid squares are squares so the diagonal is a 45 degree angle. We can determine which triangle the query point
  672. // falls in just by checking if (x + y) < 1. This arbitrarily assigns points on the diagonal to the upper triangle.
  673. // We could use "<=" if we wanted them to go to the upper triangle.
  674. bool bottomTriangle = (normalizedDelta.GetX() + normalizedDelta.GetY()) < 1.0f;
  675. // We'll query for the 3 vertices of whichever triangle of the square the query point falls in.
  676. // We'll order them in counter-clockwise order and follow a mirrored pattern between the two, so that we can calculate the
  677. // normal from the results for either triangle by using (second - first) x (third - first).
  678. if (bottomTriangle)
  679. {
  680. queryPositions.emplace_back(corners[0]);
  681. queryPositions.emplace_back(corners[1]);
  682. queryPositions.emplace_back(corners[2]);
  683. }
  684. else
  685. {
  686. queryPositions.emplace_back(corners[3]);
  687. queryPositions.emplace_back(corners[2]);
  688. queryPositions.emplace_back(corners[1]);
  689. }
  690. }
  691. AZStd::vector<float> heights(queryPositions.size());
  692. AZStd::vector<bool> exists(queryPositions.size());
  693. // Since our query points are grid-aligned, we can use EXACT queries.
  694. GetHeightsSynchronous(queryPositions, Sampler::EXACT, heights, exists);
  695. for (size_t inPosIndex = 0, queryPositionIndex = 0; inPosIndex < inPositions.size(); inPosIndex++, queryPositionIndex += queryCount)
  696. {
  697. // We'll set "exists" to true *only* if all three positions for calculating the normal exists.
  698. terrainExists[inPosIndex] = exists[queryPositionIndex] && exists[queryPositionIndex + 1] && exists[queryPositionIndex + 2];
  699. // Only calculate the normal if all the queried points exist.
  700. if (terrainExists[inPosIndex])
  701. {
  702. // Combine the output heights with our query positions.
  703. for (size_t querySubindex = 0; querySubindex < queryCount; querySubindex++)
  704. {
  705. queryPositions[queryPositionIndex + querySubindex].SetZ(heights[queryPositionIndex + querySubindex]);
  706. }
  707. // We have 3 vertices for a triangle, get the normal of the triangle.
  708. normals[inPosIndex] = (queryPositions[queryPositionIndex + 1] - queryPositions[queryPositionIndex])
  709. .Cross(queryPositions[queryPositionIndex + 2] - queryPositions[queryPositionIndex])
  710. .GetNormalized();
  711. }
  712. else
  713. {
  714. normals[inPosIndex] = AZ::Vector3::CreateAxisZ();
  715. }
  716. }
  717. }
  718. void TerrainSystem::GetNormalsSynchronousBilinear(
  719. const AZStd::span<const AZ::Vector3>& inPositions,
  720. AZStd::span<AZ::Vector3> normals, AZStd::span<bool> terrainExists) const
  721. {
  722. // When querying for normals with a BILINEAR sampler, we calculate the normals at each corner of the terrain grid square that
  723. // the query point fall in then interpolate between those normals to get the final result.
  724. const float queryResolution = m_currentSettings.m_heightQueryResolution;
  725. const float twiceQueryResolution = m_currentSettings.m_heightQueryResolution * 2.0f;
  726. // We'll need a total of 12 unique positions queried to calculate the 4 normals that we'll be interpolating between.
  727. // (We need 16 non-unique positions, but we can reuse the results for the middle 4 positions)
  728. const size_t queryCount = 12;
  729. // The full set of positions to query to be able to calculate all the normals.
  730. AZStd::vector<AZ::Vector3> queryPositions;
  731. queryPositions.reserve(inPositions.size() * queryCount);
  732. for (const auto& position : inPositions)
  733. {
  734. // We'll query our 12 points in the following order, where the x represents the location of the query point.
  735. // 10 11
  736. // *---*
  737. // 6 7| |8 9
  738. // *---*---*---*
  739. // | | x | |
  740. // *---*---*---*
  741. // 2 3| |4 5
  742. // *---*
  743. // 0 1
  744. // For each position, determine where in the square the point falls and get the bottom left corner of the center square
  745. // (corner 3).
  746. AZ::Vector2 normalizedDelta;
  747. AZ::Vector2 pos3;
  748. ClampPosition(position.GetX(), position.GetY(), queryResolution, pos3, normalizedDelta);
  749. // Corners 0-1
  750. queryPositions.emplace_back(pos3.GetX() , pos3.GetY() - queryResolution, 0.0f);
  751. queryPositions.emplace_back(pos3.GetX() + queryResolution , pos3.GetY() - queryResolution, 0.0f);
  752. // Corners 2-5
  753. queryPositions.emplace_back(pos3.GetX() - queryResolution , pos3.GetY(), 0.0f);
  754. queryPositions.emplace_back(pos3.GetX() , pos3.GetY(), 0.0f);
  755. queryPositions.emplace_back(pos3.GetX() + queryResolution , pos3.GetY(), 0.0f);
  756. queryPositions.emplace_back(pos3.GetX() + twiceQueryResolution , pos3.GetY(), 0.0f);
  757. // Corners 6-9
  758. queryPositions.emplace_back(pos3.GetX() - queryResolution , pos3.GetY() + queryResolution, 0.0f);
  759. queryPositions.emplace_back(pos3.GetX() , pos3.GetY() + queryResolution, 0.0f);
  760. queryPositions.emplace_back(pos3.GetX() + queryResolution , pos3.GetY() + queryResolution, 0.0f);
  761. queryPositions.emplace_back(pos3.GetX() + twiceQueryResolution , pos3.GetY() + queryResolution, 0.0f);
  762. // Corners 10-11
  763. queryPositions.emplace_back(pos3.GetX() , pos3.GetY() + twiceQueryResolution, 0.0f);
  764. queryPositions.emplace_back(pos3.GetX() + queryResolution , pos3.GetY() + twiceQueryResolution, 0.0f);
  765. }
  766. AZStd::vector<float> heights(queryPositions.size());
  767. AZStd::vector<bool> exists(queryPositions.size());
  768. // Since our query points are grid-aligned, we can use EXACT queries.
  769. GetHeightsSynchronous(queryPositions, Sampler::EXACT, heights, exists);
  770. for (size_t inPosIndex = 0, queryPositionIndex = 0; inPosIndex < inPositions.size(); inPosIndex++, queryPositionIndex += queryCount)
  771. {
  772. // Combine the output heights with our query positions.
  773. for (size_t querySubindex = 0; querySubindex < queryCount; querySubindex++)
  774. {
  775. queryPositions[queryPositionIndex + querySubindex].SetZ(heights[queryPositionIndex + querySubindex]);
  776. }
  777. // We calculate the normal by taking the cross product of the tips of a + shape around the point that we want the normal for.
  778. auto CalculateNormal = [&exists, &queryPositions, &queryPositionIndex]
  779. (uint8_t left, uint8_t center, uint8_t right, uint8_t up, uint8_t down) -> AZ::Vector3
  780. {
  781. const size_t centerQueryIdx = queryPositionIndex + center;
  782. const size_t upQueryIdx = queryPositionIndex + up;
  783. const size_t downQueryIdx = queryPositionIndex + down;
  784. const size_t leftQueryIdx = queryPositionIndex + left;
  785. const size_t rightQueryIdx = queryPositionIndex + right;
  786. // Only calculate the normal if the center point and all the points around it exist. Otherwise, just return a Z-up vector.
  787. if (exists[upQueryIdx] && exists[downQueryIdx] && exists[leftQueryIdx] && exists[centerQueryIdx] && exists[rightQueryIdx])
  788. {
  789. // Each normal is (right - left) x (up - down)
  790. return (queryPositions[rightQueryIdx] - queryPositions[leftQueryIdx])
  791. .Cross(queryPositions[upQueryIdx] - queryPositions[downQueryIdx])
  792. .GetNormalized();
  793. }
  794. else
  795. {
  796. return AZ::Vector3::CreateAxisZ();
  797. }
  798. };
  799. // Calculate the normals of the four corners of the square that our query point falls in by taking the cross product
  800. // of + shapes:
  801. // 10 11 normal0 normal1 normal2 normal3
  802. // *---* 7 8 10 11
  803. // 6 7| |8 9 * * * *
  804. // *---*---*---* 2 |3 4 3 |4 5 6 |7 8 7 |8 9
  805. // | | x | | *---*---* *---*---* *---*---* *---*---*
  806. // *---*---*---* | | | |
  807. // 2 3| |4 5 * * * *
  808. // *---* 0 1 3 4
  809. // 0 1
  810. const AZ::Vector3 normal0 = CalculateNormal(2, 3, 4, 7, 0);
  811. const AZ::Vector3 normal1 = CalculateNormal(3, 4, 5, 8, 1);
  812. const AZ::Vector3 normal2 = CalculateNormal(6, 7, 8, 10, 3);
  813. const AZ::Vector3 normal3 = CalculateNormal(7, 8, 9, 11, 4);
  814. // For each position, determine where in the square the point falls and get the bottom left corner of the center square
  815. // (corner 3).
  816. AZ::Vector2 normalizedDelta;
  817. AZ::Vector2 pos3;
  818. ClampPosition(inPositions[inPosIndex].GetX(), inPositions[inPosIndex].GetY(), queryResolution, pos3, normalizedDelta);
  819. // Then finally, interpolate between the 4 normals.
  820. const float lerpX = normalizedDelta.GetX();
  821. const float lerpY = normalizedDelta.GetY();
  822. const float invLerpX = 1.0f - lerpX;
  823. const float invLerpY = 1.0f - lerpY;
  824. AZ::Vector3 combinedNormal =
  825. (normal0 * (invLerpX * invLerpY)) +
  826. (normal1 * (lerpX * invLerpY)) +
  827. (normal2 * (invLerpX * lerpY)) +
  828. (normal3 * (lerpX * lerpY));
  829. normals[inPosIndex] = combinedNormal.GetNormalized();
  830. // Use the "terrain exists" result from the nearest corner as the result we'll return.
  831. if ((lerpX < 0.5f) && (lerpY < 0.5f))
  832. {
  833. terrainExists[inPosIndex] = exists[queryPositionIndex + 3];
  834. }
  835. else if ((lerpX >= 0.5f) && (lerpY < 0.5f))
  836. {
  837. terrainExists[inPosIndex] = exists[queryPositionIndex + 4];
  838. }
  839. else if ((lerpX < 0.5f) && (lerpY >= 0.5f))
  840. {
  841. terrainExists[inPosIndex] = exists[queryPositionIndex + 7];
  842. }
  843. else
  844. {
  845. terrainExists[inPosIndex] = exists[queryPositionIndex + 8];
  846. }
  847. }
  848. }
  849. AZ::Vector3 TerrainSystem::GetNormalSynchronous(const AZ::Vector3& position, Sampler sampler, bool* terrainExistsPtr) const
  850. {
  851. AZ::Vector3 normal;
  852. bool exists;
  853. GetNormalsSynchronous(
  854. AZStd::span<const AZ::Vector3>(&position, 1), sampler, AZStd::span<AZ::Vector3>(&normal, 1), AZStd::span<bool>(&exists, 1));
  855. if (terrainExistsPtr)
  856. {
  857. *terrainExistsPtr = exists;
  858. }
  859. return normal;
  860. }
  861. AZ::Vector3 TerrainSystem::GetNormal(const AZ::Vector3& position, Sampler sampler, bool* terrainExistsPtr) const
  862. {
  863. return GetNormalSynchronous(position, sampler, terrainExistsPtr);
  864. }
  865. AZ::Vector3 TerrainSystem::GetNormalFromVector2(const AZ::Vector2& position, Sampler sampler, bool* terrainExistsPtr) const
  866. {
  867. return GetNormalSynchronous(AZ::Vector3(position), sampler, terrainExistsPtr);
  868. }
  869. AZ::Vector3 TerrainSystem::GetNormalFromFloats(float x, float y, Sampler sampler, bool* terrainExistsPtr) const
  870. {
  871. return GetNormalSynchronous(AZ::Vector3(x, y, 0.0f), sampler, terrainExistsPtr);
  872. }
  873. AzFramework::SurfaceData::SurfaceTagWeight TerrainSystem::GetMaxSurfaceWeight(
  874. const AZ::Vector3& position, Sampler sampler, bool* terrainExistsPtr) const
  875. {
  876. return GetMaxSurfaceWeightFromFloats(position.GetX(), position.GetY(), sampler, terrainExistsPtr);
  877. }
  878. AzFramework::SurfaceData::SurfaceTagWeight TerrainSystem::GetMaxSurfaceWeightFromVector2(
  879. const AZ::Vector2& inPosition, Sampler sampler, bool* terrainExistsPtr) const
  880. {
  881. return GetMaxSurfaceWeightFromFloats(inPosition.GetX(), inPosition.GetY(), sampler, terrainExistsPtr);
  882. }
  883. AzFramework::SurfaceData::SurfaceTagWeight TerrainSystem::GetMaxSurfaceWeightFromFloats(
  884. const float x, const float y, Sampler sampler, bool* terrainExistsPtr) const
  885. {
  886. if (terrainExistsPtr)
  887. {
  888. *terrainExistsPtr = true;
  889. }
  890. AzFramework::SurfaceData::SurfaceTagWeightList weightSet;
  891. GetOrderedSurfaceWeights(x, y, sampler, weightSet, terrainExistsPtr);
  892. if (weightSet.empty())
  893. {
  894. return {};
  895. }
  896. return *weightSet.begin();
  897. }
  898. void TerrainSystem::GetSurfacePoint(
  899. const AZ::Vector3& inPosition,
  900. AzFramework::SurfaceData::SurfacePoint& outSurfacePoint,
  901. Sampler sampler,
  902. bool* terrainExistsPtr) const
  903. {
  904. // Query normals before heights because the height query produces better results for the terrainExists flag for a given point,
  905. // so we want to prefer keeping the results from the height query if we end up querying both.
  906. // (Ideally at some point they will produce identical results)
  907. outSurfacePoint.m_normal = GetNormalSynchronous(inPosition, sampler, terrainExistsPtr);
  908. outSurfacePoint.m_position = inPosition;
  909. outSurfacePoint.m_position.SetZ(GetHeightSynchronous(inPosition.GetX(), inPosition.GetY(), sampler, terrainExistsPtr));
  910. GetSurfaceWeights(inPosition, outSurfacePoint.m_surfaceTags, sampler, nullptr);
  911. }
  912. void TerrainSystem::GetSurfacePointFromVector2(
  913. const AZ::Vector2& inPosition,
  914. AzFramework::SurfaceData::SurfacePoint& outSurfacePoint,
  915. Sampler sampler,
  916. bool* terrainExistsPtr) const
  917. {
  918. GetSurfacePoint(AZ::Vector3(inPosition.GetX(), inPosition.GetY(), 0.0f), outSurfacePoint, sampler, terrainExistsPtr);
  919. }
  920. void TerrainSystem::GetSurfacePointFromFloats(
  921. float x,
  922. float y,
  923. AzFramework::SurfaceData::SurfacePoint& outSurfacePoint,
  924. Sampler sampler,
  925. bool* terrainExistsPtr) const
  926. {
  927. GetSurfacePoint(AZ::Vector3(x, y, 0.0f), outSurfacePoint, sampler, terrainExistsPtr);
  928. }
  929. AzFramework::EntityContextId TerrainSystem::GetTerrainRaycastEntityContextId() const
  930. {
  931. return m_terrainRaycastContext.GetEntityContextId();
  932. }
  933. AzFramework::RenderGeometry::RayResult TerrainSystem::GetClosestIntersection(
  934. const AzFramework::RenderGeometry::RayRequest& ray) const
  935. {
  936. return m_terrainRaycastContext.RayIntersect(ray);
  937. }
  938. AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> TerrainSystem::QueryListAsync(
  939. const AZStd::span<const AZ::Vector3>& inPositions,
  940. TerrainDataMask requestedData,
  941. AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
  942. Sampler sampler,
  943. AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> params) const
  944. {
  945. return ProcessFromListAsync(inPositions, requestedData, perPositionCallback, sampler, params);
  946. }
  947. AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> TerrainSystem::QueryListOfVector2Async(
  948. const AZStd::span<const AZ::Vector2>& inPositions,
  949. TerrainDataMask requestedData,
  950. AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
  951. Sampler sampler,
  952. AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> params) const
  953. {
  954. return ProcessFromListAsync(inPositions, requestedData, perPositionCallback, sampler, params);
  955. }
  956. AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> TerrainSystem::QueryRegionAsync(
  957. const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
  958. TerrainDataMask requestedData,
  959. AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
  960. Sampler sampler,
  961. AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> params) const
  962. {
  963. AZ_PROFILE_FUNCTION(Terrain);
  964. auto numSamplesX = queryRegion.m_numPointsX;
  965. auto numSamplesY = queryRegion.m_numPointsY;
  966. const int64_t numPositionsToProcess = numSamplesX * numSamplesY;
  967. if (numPositionsToProcess == 0)
  968. {
  969. AZ_Warning("TerrainSystem", false, "No positions to process.");
  970. return nullptr;
  971. }
  972. // Determine the maximum number of jobs, and the minimum number of positions that should be processed per job.
  973. const int32_t numJobsMax = CalculateMaxJobs(params);
  974. const int32_t minPositionsPerJob = params && (params->m_minPositionsPerJob > 0)
  975. ? params->m_minPositionsPerJob
  976. : AzFramework::Terrain::QueryAsyncParams::MinPositionsPerJobDefault;
  977. // Calculate the best subdivision of the region along both the X and Y axes to use as close to the maximum number of jobs
  978. // as possible while also keeping all the regions effectively the same size.
  979. int32_t xJobs, yJobs;
  980. SubdivideRegionForJobs(
  981. aznumeric_cast<int32_t>(numSamplesX), aznumeric_cast<int32_t>(numSamplesY), numJobsMax, minPositionsPerJob, xJobs, yJobs);
  982. // The number of jobs returned might be less than the total requested maximum number of jobs, so recalculate it here
  983. const int32_t numJobs = xJobs * yJobs;
  984. // Get the number of samples in each direction that we'll use for each query. We calculate this as a fractional value
  985. // so that we can keep each query pretty evenly balanced, with just +/- 1 count variation on each axis.
  986. const float xSamplesPerQuery = aznumeric_cast<float>(numSamplesX) / xJobs;
  987. const float ySamplesPerQuery = aznumeric_cast<float>(numSamplesY) / yJobs;
  988. // Make sure our subdivisions are producing at least minPositionsPerJob unless the *total* requested point count is
  989. // less than minPositionsPerJob.
  990. AZ_Assert(
  991. ((numSamplesX * numSamplesY) < minPositionsPerJob) ||
  992. (aznumeric_cast<int32_t>(xSamplesPerQuery) * aznumeric_cast<int32_t>(ySamplesPerQuery)) >= minPositionsPerJob,
  993. "Too few positions per job: %d vs %d", aznumeric_cast<int32_t>(xSamplesPerQuery) * aznumeric_cast<int32_t>(ySamplesPerQuery),
  994. minPositionsPerJob);
  995. // Create a terrain job context and split the work across multiple jobs.
  996. AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> jobContext =
  997. AZStd::make_shared<AzFramework::Terrain::TerrainJobContext>(*m_terrainJobManager, numJobs);
  998. {
  999. AZStd::unique_lock<AZStd::mutex> lock(m_activeTerrainJobContextMutex);
  1000. m_activeTerrainJobContexts.push_back(jobContext);
  1001. }
  1002. [[maybe_unused]] int32_t jobsStarted = 0;
  1003. for (int32_t yJob = 0; yJob < yJobs; yJob++)
  1004. {
  1005. // Use the fractional samples per query to calculate the start and end of the region, but then convert it
  1006. // back to integers so that our regions are always in exact multiples of the number of samples to process.
  1007. // This is important because we want the XY values for each point that we're processing to exactly align with
  1008. // 'start + N * (step size)', or else we'll start to process point locations that weren't actually what was requested.
  1009. const int32_t y0 = aznumeric_cast<int32_t>(AZStd::lround(yJob * ySamplesPerQuery));
  1010. const int32_t y1 = aznumeric_cast<int32_t>(AZStd::lround((yJob + 1) * ySamplesPerQuery));
  1011. const float inRegionMinY = queryRegion.m_startPoint.GetY() + (y0 * queryRegion.m_stepSize.GetY());
  1012. const int32_t numPointsY = AZStd::min(y1 - y0, aznumeric_cast<int32_t>(numSamplesY) - y0);
  1013. for (int32_t xJob = 0; xJob < xJobs; xJob++)
  1014. {
  1015. // Same as above, calculate the start and end of the region, then convert back to integers and create the
  1016. // region based on 'start + n * (step size)'.
  1017. const int32_t x0 = aznumeric_cast<int32_t>(AZStd::lround(xJob * xSamplesPerQuery));
  1018. const int32_t x1 = aznumeric_cast<int32_t>(AZStd::lround((xJob + 1) * xSamplesPerQuery));
  1019. const float inRegionMinX = queryRegion.m_startPoint.GetX() + (x0 * queryRegion.m_stepSize.GetX());
  1020. const int32_t numPointsX = AZStd::min(x1 - x0, aznumeric_cast<int32_t>(numSamplesX) - x0);
  1021. // Define the job function using the sub region of positions to process.
  1022. AzFramework::Terrain::TerrainQueryRegion subQueryRegion(
  1023. AZ::Vector3(inRegionMinX, inRegionMinY, queryRegion.m_startPoint.GetZ()), numPointsX, numPointsY, queryRegion.m_stepSize);
  1024. auto jobFunction = [this, subQueryRegion, x0, y0, requestedData, perPositionCallback, sampler, jobContext, params]()
  1025. {
  1026. // Process the sub region of positions, unless the associated job context has been cancelled.
  1027. if (!jobContext->IsCancelled())
  1028. {
  1029. QueryRegionInternal(subQueryRegion, x0, y0, requestedData, perPositionCallback, sampler);
  1030. }
  1031. // Decrement the number of completions remaining, invoke the completion callback if this happens
  1032. // to be the final job completed, and remove this TerrainJobContext from the list of active ones.
  1033. const bool wasLastJobCompleted = jobContext->OnJobCompleted();
  1034. if (wasLastJobCompleted)
  1035. {
  1036. if (params && params->m_completionCallback)
  1037. {
  1038. params->m_completionCallback(jobContext);
  1039. }
  1040. {
  1041. AZStd::unique_lock<AZStd::mutex> lock(m_activeTerrainJobContextMutex);
  1042. m_activeTerrainJobContexts.erase(
  1043. AZStd::find(m_activeTerrainJobContexts.begin(), m_activeTerrainJobContexts.end(), jobContext));
  1044. m_activeTerrainJobContextMutexConditionVariable.notify_one();
  1045. }
  1046. }
  1047. };
  1048. // Create the job and start it immediately.
  1049. AZ::Job* processJob = AZ::CreateJobFunction(jobFunction, true, jobContext.get());
  1050. processJob->Start();
  1051. jobsStarted++;
  1052. }
  1053. }
  1054. // Validate this just to ensure that the fractional math for handling points didn't cause any rounding errors anywhere.
  1055. AZ_Assert(jobsStarted == numJobs, "Wrong number of jobs created: %d vs %d", jobsStarted, numJobs);
  1056. return jobContext;
  1057. }
  1058. AZ::EntityId TerrainSystem::FindBestAreaEntityAtPosition(const AZ::Vector3& position, AZ::Aabb& bounds) const
  1059. {
  1060. // Find the highest priority layer that encompasses this position
  1061. // The areas are sorted into priority order: the first area that contains inPosition is the most suitable.
  1062. for (const auto& [areaId, areaData] : m_registeredAreas)
  1063. {
  1064. // We use min-inclusive-max-exclusive so that two spawners with a shared edge will have a single owner for that edge.
  1065. if (SurfaceData::AabbContains2DMaxExclusive(areaData.m_areaBounds, position))
  1066. {
  1067. bounds = areaData.m_areaBounds;
  1068. return areaId;
  1069. }
  1070. }
  1071. return AZ::EntityId();
  1072. }
  1073. void TerrainSystem::GetOrderedSurfaceWeightsFromList(
  1074. const AZStd::span<const AZ::Vector3>& inPositions,
  1075. Sampler sampler,
  1076. AZStd::span<AzFramework::SurfaceData::SurfaceTagWeightList> outSurfaceWeightsList,
  1077. AZStd::span<bool> terrainExists) const
  1078. {
  1079. TERRAIN_PROFILE_FUNCTION_VERBOSE
  1080. if (terrainExists.size() == outSurfaceWeightsList.size())
  1081. {
  1082. AZStd::vector<float> heights(inPositions.size());
  1083. GetHeightsSynchronous(inPositions, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, heights, terrainExists);
  1084. }
  1085. // queryPositions contains the modified positions based on our sampler type. For surface queries, we don't currently perform bilinear
  1086. // interpolation of any results, so our query position size will always match our input size.
  1087. AZStd::vector<AZ::Vector3> queryPositions;
  1088. queryPositions.reserve(inPositions.size());
  1089. const float queryResolution = m_currentSettings.m_surfaceDataQueryResolution;
  1090. Sampler querySampler = (sampler == Sampler::EXACT) ? Sampler::EXACT : Sampler::CLAMP;
  1091. GenerateQueryPositions(inPositions, queryPositions, queryResolution, querySampler);
  1092. auto callback = [](const AZStd::span<const AZ::Vector3> inPositions,
  1093. [[maybe_unused]] AZStd::span<AZ::Vector3> outPositions,
  1094. [[maybe_unused]] AZStd::span<bool> outTerrainExists,
  1095. AZStd::span<AzFramework::SurfaceData::SurfaceTagWeightList> outSurfaceWeights,
  1096. AZ::EntityId areaId)
  1097. {
  1098. AZ_Assert(inPositions.size() == outSurfaceWeights.size(),
  1099. "The sizes of the surface weights list and in/out positions list should match.");
  1100. Terrain::TerrainAreaSurfaceRequestBus::Event(areaId, &Terrain::TerrainAreaSurfaceRequestBus::Events::GetSurfaceWeightsFromList,
  1101. inPositions, outSurfaceWeights);
  1102. // Sort the surface weights on each output weight list in decreasing weight order.
  1103. for (auto& outSurfaceWeight : outSurfaceWeights)
  1104. {
  1105. AZStd::sort(
  1106. outSurfaceWeight.begin(), outSurfaceWeight.end(),
  1107. AzFramework::SurfaceData::SurfaceTagWeightComparator());
  1108. }
  1109. };
  1110. // This will be unused for surface weights. It's fine if it's empty.
  1111. AZStd::vector<AZ::Vector3> outPositions;
  1112. MakeBulkQueries(queryPositions, outPositions, terrainExists, outSurfaceWeightsList, callback);
  1113. }
  1114. void TerrainSystem::GetOrderedSurfaceWeights(
  1115. const float x,
  1116. const float y,
  1117. Sampler sampler,
  1118. AzFramework::SurfaceData::SurfaceTagWeightList& outSurfaceWeights,
  1119. bool* terrainExistsPtr) const
  1120. {
  1121. AZStd::shared_lock<AZStd::shared_mutex> lock(m_areaMutex);
  1122. if (terrainExistsPtr)
  1123. {
  1124. GetHeightFromFloats(x, y, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, terrainExistsPtr);
  1125. }
  1126. outSurfaceWeights.clear();
  1127. const float queryResolution = m_currentSettings.m_surfaceDataQueryResolution;
  1128. AZ::Vector3 inPosition;
  1129. switch (sampler)
  1130. {
  1131. // Both bilinear and clamp samplers will clamp the input position to the surface data query grid and get the surface data there.
  1132. // At some point we might want to consider interpolation of surface weights for the bilinear case, but it's unclear if that's
  1133. // actually a desired outcome.
  1134. case AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR:
  1135. [[fallthrough]];
  1136. case AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP:
  1137. {
  1138. AZ::Vector2 clampedPosition;
  1139. RoundPosition(x, y, queryResolution, clampedPosition);
  1140. inPosition = AZ::Vector3(clampedPosition);
  1141. }
  1142. break;
  1143. //! Directly get the value at the location, regardless of terrain sample grid density.
  1144. case AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT:
  1145. [[fallthrough]];
  1146. default:
  1147. inPosition = AZ::Vector3(x, y, 0.0f);
  1148. break;
  1149. }
  1150. AZ::Aabb bounds;
  1151. AZ::EntityId bestAreaId = FindBestAreaEntityAtPosition(inPosition, bounds);
  1152. if (!bestAreaId.IsValid())
  1153. {
  1154. return;
  1155. }
  1156. // Get all the surfaces with weights at the given point.
  1157. Terrain::TerrainAreaSurfaceRequestBus::Event(
  1158. bestAreaId, &Terrain::TerrainAreaSurfaceRequestBus::Events::GetSurfaceWeights, inPosition, outSurfaceWeights);
  1159. AZStd::sort(outSurfaceWeights.begin(), outSurfaceWeights.end(), AzFramework::SurfaceData::SurfaceTagWeightComparator());
  1160. }
  1161. void TerrainSystem::GetSurfaceWeights(
  1162. const AZ::Vector3& inPosition,
  1163. AzFramework::SurfaceData::SurfaceTagWeightList& outSurfaceWeights,
  1164. Sampler sampler,
  1165. bool* terrainExistsPtr) const
  1166. {
  1167. GetOrderedSurfaceWeights(inPosition.GetX(), inPosition.GetY(), sampler, outSurfaceWeights, terrainExistsPtr);
  1168. }
  1169. void TerrainSystem::GetSurfaceWeightsFromVector2(
  1170. const AZ::Vector2& inPosition,
  1171. AzFramework::SurfaceData::SurfaceTagWeightList& outSurfaceWeights,
  1172. Sampler sampler,
  1173. bool* terrainExistsPtr) const
  1174. {
  1175. GetOrderedSurfaceWeights(inPosition.GetX(), inPosition.GetY(), sampler, outSurfaceWeights, terrainExistsPtr);
  1176. }
  1177. void TerrainSystem::GetSurfaceWeightsFromFloats(
  1178. float x, float y,
  1179. AzFramework::SurfaceData::SurfaceTagWeightList& outSurfaceWeights,
  1180. Sampler sampler,
  1181. bool* terrainExistsPtr) const
  1182. {
  1183. GetOrderedSurfaceWeights(x, y, sampler, outSurfaceWeights, terrainExistsPtr);
  1184. }
  1185. const char* TerrainSystem::GetMaxSurfaceName(
  1186. [[maybe_unused]] const AZ::Vector3& position, [[maybe_unused]] Sampler sampler, [[maybe_unused]] bool* terrainExistsPtr) const
  1187. {
  1188. // For now, always set terrainExists to true, as we don't have a way to author data for terrain holes yet.
  1189. if (terrainExistsPtr)
  1190. {
  1191. *terrainExistsPtr = true;
  1192. }
  1193. return "";
  1194. }
  1195. void TerrainSystem::QueryList(
  1196. const AZStd::span<const AZ::Vector3>& inPositions,
  1197. TerrainDataMask requestedData,
  1198. AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
  1199. Sampler sampler) const
  1200. {
  1201. TERRAIN_PROFILE_FUNCTION_VERBOSE
  1202. if (!perPositionCallback)
  1203. {
  1204. return;
  1205. }
  1206. AZStd::vector<bool> terrainExists(inPositions.size());
  1207. AZStd::vector<float> heights;
  1208. AZStd::vector<AZ::Vector3> normals;
  1209. AZStd::vector<AzFramework::SurfaceData::SurfaceTagWeightList> surfaceWeights;
  1210. // Query normals before heights because the height query produces better results for the terrainExists flag for a given point,
  1211. // so we want to prefer keeping the results from the height query if we end up querying both.
  1212. // (Ideally at some point they will produce identical results)
  1213. if (requestedData & TerrainDataMask::Normals)
  1214. {
  1215. normals.resize(inPositions.size());
  1216. GetNormalsSynchronous(inPositions, sampler, normals, terrainExists);
  1217. }
  1218. if (requestedData & TerrainDataMask::Heights)
  1219. {
  1220. heights.resize(inPositions.size());
  1221. GetHeightsSynchronous(inPositions, sampler, heights, terrainExists);
  1222. }
  1223. if (requestedData & TerrainDataMask::SurfaceData)
  1224. {
  1225. // We can potentially skip an extra call to GetHeights if we already
  1226. // got the terrain exists flags in the earlier call to GetHeights
  1227. AZStd::vector<bool> terrainExistsEmpty;
  1228. surfaceWeights.resize(inPositions.size());
  1229. GetOrderedSurfaceWeightsFromList(inPositions, sampler, surfaceWeights,
  1230. (requestedData & TerrainDataMask::Heights) ? terrainExistsEmpty : terrainExists);
  1231. }
  1232. {
  1233. TERRAIN_PROFILE_SCOPE_VERBOSE("QueryList-PerPositionCallbacks");
  1234. AzFramework::SurfaceData::SurfacePoint surfacePoint;
  1235. for (size_t i = 0; i < inPositions.size(); i++)
  1236. {
  1237. surfacePoint.m_position = inPositions[i];
  1238. if (requestedData & TerrainDataMask::Heights)
  1239. {
  1240. surfacePoint.m_position.SetZ(heights[i]);
  1241. }
  1242. if (requestedData & TerrainDataMask::Normals)
  1243. {
  1244. surfacePoint.m_normal = AZStd::move(normals[i]);
  1245. }
  1246. if (requestedData & TerrainDataMask::SurfaceData)
  1247. {
  1248. surfacePoint.m_surfaceTags = AZStd::move(surfaceWeights[i]);
  1249. }
  1250. perPositionCallback(surfacePoint, terrainExists[i]);
  1251. }
  1252. }
  1253. }
  1254. void TerrainSystem::QueryListOfVector2(
  1255. const AZStd::span<const AZ::Vector2>& inPositions,
  1256. TerrainDataMask requestedData,
  1257. AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
  1258. Sampler sampler) const
  1259. {
  1260. AZ_PROFILE_FUNCTION(Terrain);
  1261. if (!perPositionCallback)
  1262. {
  1263. return;
  1264. }
  1265. AZStd::vector<AZ::Vector3> inPositionsVec3 = GenerateInputPositionsFromListOfVector2(inPositions);
  1266. QueryList(inPositionsVec3, requestedData, perPositionCallback, sampler);
  1267. }
  1268. //! Given a set of async parameters, calculate the max number of jobs that we can use for the async call.
  1269. int32_t TerrainSystem::CalculateMaxJobs(AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> params) const
  1270. {
  1271. // Determine the maximum number of jobs available to split the work across for async calls.
  1272. const int32_t numWorkerThreads = m_terrainJobManager->GetNumWorkerThreads();
  1273. const int32_t numJobsDesired = params ? params->m_desiredNumberOfJobs : AzFramework::Terrain::QueryAsyncParams::NumJobsDefault;
  1274. const int32_t numJobsMax = (numJobsDesired > 0) ? AZStd::min(numWorkerThreads, numJobsDesired) : numWorkerThreads;
  1275. return numJobsMax;
  1276. }
  1277. void TerrainSystem::SubdivideRegionForJobs(
  1278. int32_t numSamplesX, int32_t numSamplesY, int32_t maxNumJobs, int32_t minPointsPerJob, int32_t& subdivisionsX, int32_t& subdivisionsY)
  1279. {
  1280. // This method will try to determine the best way to distribute the number of X and Y samples in our region across as many jobs
  1281. // as possible to meet the following constraints:
  1282. // subdivisionsX * subdivisionsY <= maxNumJobs
  1283. // (numSamplesX / subdivisionsX) * (numSamplesY / subdivisionsY) >= minPointsPerJob
  1284. // Basically, the goal is to use the maximum number of jobs, as long as we're processing at least the minimum points per job.
  1285. // We also try to keep the subdivisions of X as low as possible because it's generally more efficient to process
  1286. // consecutive X values than consecutive Y values.
  1287. // Start by initializing to a single job that processes the entire region.
  1288. subdivisionsX = 1;
  1289. subdivisionsY = 1;
  1290. int32_t bestJobUsage = 1;
  1291. // If the entire region is less than the minimum points, just return.
  1292. if ((numSamplesX * numSamplesY) < minPointsPerJob)
  1293. {
  1294. return;
  1295. }
  1296. // Clamp the maximum number of jobs to whichever is smaller - the maximum number of jobs that have minPointsPerJob, or the
  1297. // requested maxNumJobs. We can't have a solution that violates either constraint.
  1298. int32_t clampedMaxNumJobs = AZStd::clamp(aznumeric_cast<int32_t>((numSamplesX * numSamplesY) / minPointsPerJob), 1, maxNumJobs);
  1299. // MaxNumJobs will generally be a small value, so we can just brute-force the problem and try every solution to see what will
  1300. // produce the most optimal results. We stop early if we find a solution that uses the maximum number of jobs.
  1301. // We loop on X subdivisions first so that we bias towards solutions with a lower number of X subdivisions.
  1302. for (int32_t xChoice = 1; xChoice <= clampedMaxNumJobs; xChoice++)
  1303. {
  1304. // For a given number of X subdivisions, find the maximum number of Y subdivisions that produces at least the
  1305. // minimum number of points per job.
  1306. int32_t xSamplesPerSubdivision = numSamplesX / xChoice;
  1307. // This is how many rows of X we need to produce minPointsPerJob.
  1308. int32_t minXRowsNeeded = aznumeric_cast<int32_t>(AZStd::ceil(aznumeric_cast<float>(minPointsPerJob) / xSamplesPerSubdivision));
  1309. // Get the maximum number of subdivisions for Y that will produce minPointsPerJob (numSamplesY / minXRowsNeeded), but
  1310. // also clamp it by the maximum number of jobs that we're allowed to produce (maxNumJobs / xChoice).
  1311. int32_t yChoice = AZStd::min(numSamplesY / minXRowsNeeded, clampedMaxNumJobs / xChoice);
  1312. // The maximum number of subdivisions in Y will decrease with increasing X subdivisions. If we've reached the point
  1313. // where even the entire Y range (i.e. yChoice == 1) isn't sufficient, we can stop checking, we won't find any more solutions.
  1314. if (yChoice == 0)
  1315. {
  1316. break;
  1317. }
  1318. // If this combination is better than a previous solution, save it as our new best solution.
  1319. int32_t jobUsage = xChoice * yChoice;
  1320. if (jobUsage > bestJobUsage)
  1321. {
  1322. subdivisionsX = xChoice;
  1323. subdivisionsY = yChoice;
  1324. bestJobUsage = jobUsage;
  1325. // If we've found an optimal solution, early-out.
  1326. if (jobUsage == clampedMaxNumJobs)
  1327. {
  1328. break;
  1329. }
  1330. }
  1331. }
  1332. // Verify that our subdivision strategy has stayed within the max jobs constraint.
  1333. AZ_Assert(
  1334. (subdivisionsX * subdivisionsY) <= maxNumJobs, "The region was subdivided into too many jobs: %d x %d vs %d max", subdivisionsX,
  1335. subdivisionsY, maxNumJobs);
  1336. }
  1337. bool TerrainSystem::ContainedAabbTouchesEdge(const AZ::Aabb& outerAabb, const AZ::Aabb& innerAabb)
  1338. {
  1339. return outerAabb.Contains(innerAabb) &&
  1340. (
  1341. outerAabb.GetMin().GetX() == innerAabb.GetMin().GetX() ||
  1342. outerAabb.GetMin().GetY() == innerAabb.GetMin().GetY() ||
  1343. outerAabb.GetMin().GetZ() == innerAabb.GetMin().GetZ() ||
  1344. outerAabb.GetMax().GetX() == innerAabb.GetMax().GetX() ||
  1345. outerAabb.GetMax().GetY() == innerAabb.GetMax().GetY() ||
  1346. outerAabb.GetMax().GetZ() == innerAabb.GetMax().GetZ()
  1347. );
  1348. }
  1349. void TerrainSystem::QueryRegion(
  1350. const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
  1351. TerrainDataMask requestedData,
  1352. AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
  1353. Sampler sampler) const
  1354. {
  1355. QueryRegionInternal(queryRegion, 0, 0, requestedData, perPositionCallback, sampler);
  1356. }
  1357. void TerrainSystem::QueryRegionInternal(
  1358. const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
  1359. size_t xIndexOffset, size_t yIndexOffset,
  1360. TerrainDataMask requestedData,
  1361. AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
  1362. Sampler sampler) const
  1363. {
  1364. AZ_PROFILE_FUNCTION(Terrain);
  1365. // Don't bother processing if we don't have a callback
  1366. if (!perPositionCallback)
  1367. {
  1368. return;
  1369. }
  1370. AZStd::vector<AZ::Vector3> inPositions = GenerateInputPositionsFromRegion(queryRegion);
  1371. if (inPositions.empty())
  1372. {
  1373. return;
  1374. }
  1375. AZStd::vector<bool> terrainExists(inPositions.size());
  1376. AZStd::vector<float> heights;
  1377. AZStd::vector<AZ::Vector3> normals;
  1378. AZStd::vector<AzFramework::SurfaceData::SurfaceTagWeightList> surfaceWeights;
  1379. // Query normals before heights because the height query produces better results for the terrainExists flag for a given point,
  1380. // so we want to prefer keeping the results from the height query if we end up querying both.
  1381. // (Ideally at some point they will produce identical results)
  1382. if (requestedData & TerrainDataMask::Normals)
  1383. {
  1384. normals.resize(inPositions.size());
  1385. {
  1386. AZ_PROFILE_SCOPE(Terrain, "GetNormalsSynchronous");
  1387. GetNormalsSynchronous(inPositions, sampler, normals, terrainExists);
  1388. }
  1389. }
  1390. if (requestedData & TerrainDataMask::Heights)
  1391. {
  1392. heights.resize(inPositions.size());
  1393. GetHeightsSynchronous(inPositions, sampler, heights, terrainExists);
  1394. }
  1395. if (requestedData & TerrainDataMask::SurfaceData)
  1396. {
  1397. // We can potentially skip an extra call to GetHeights if we already
  1398. // got the terrain exists flags in the earlier call to GetHeights
  1399. AZStd::vector<bool> terrainExistsEmpty;
  1400. surfaceWeights.resize(inPositions.size());
  1401. GetOrderedSurfaceWeightsFromList(
  1402. inPositions, sampler, surfaceWeights, (requestedData & TerrainDataMask::Heights) ? terrainExistsEmpty : terrainExists);
  1403. }
  1404. {
  1405. AZ_PROFILE_SCOPE(Terrain, "QueryRegionInternal-PerPositionCallbacks");
  1406. AzFramework::SurfaceData::SurfacePoint surfacePoint;
  1407. for (size_t y = 0, i = 0; y < queryRegion.m_numPointsY; y++)
  1408. {
  1409. for (size_t x = 0; x < queryRegion.m_numPointsX; x++)
  1410. {
  1411. surfacePoint.m_position = inPositions[i];
  1412. if (requestedData & TerrainDataMask::Heights)
  1413. {
  1414. surfacePoint.m_position.SetZ(heights[i]);
  1415. }
  1416. if (requestedData & TerrainDataMask::Normals)
  1417. {
  1418. surfacePoint.m_normal = AZStd::move(normals[i]);
  1419. }
  1420. if (requestedData & TerrainDataMask::SurfaceData)
  1421. {
  1422. surfacePoint.m_surfaceTags = AZStd::move(surfaceWeights[i]);
  1423. }
  1424. perPositionCallback(x + xIndexOffset, y + yIndexOffset, surfacePoint, terrainExists[i]);
  1425. i++;
  1426. }
  1427. }
  1428. }
  1429. }
  1430. void TerrainSystem::RegisterArea(AZ::EntityId areaId)
  1431. {
  1432. AZStd::unique_lock<AZStd::shared_mutex> lock(m_areaMutex);
  1433. AZ::Aabb aabb = AZ::Aabb::CreateNull();
  1434. LmbrCentral::ShapeComponentRequestsBus::EventResult(aabb, areaId, &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb);
  1435. // Cache off whether or not this layer spawner should have a default ground plane when no other terrain height data exists.
  1436. bool useGroundPlane = false;
  1437. Terrain::TerrainSpawnerRequestBus::EventResult(useGroundPlane, areaId, &Terrain::TerrainSpawnerRequestBus::Events::GetUseGroundPlane);
  1438. m_registeredAreas[areaId] = { aabb, useGroundPlane };
  1439. m_dirtyRegion.AddAabb(aabb);
  1440. m_terrainDirtyMask |= AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask::HeightData |
  1441. AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask::SurfaceData;
  1442. m_cachedAreaBounds.AddAabb(aabb);
  1443. }
  1444. void TerrainSystem::UnregisterArea(AZ::EntityId areaId)
  1445. {
  1446. AZStd::unique_lock<AZStd::shared_mutex> lock(m_areaMutex);
  1447. // Remove the data for this entity from the registered areas.
  1448. // Erase_if is used as erase would use the comparator to lookup the entity id in the map.
  1449. // As the comparator will get the new layer/priority data for the entity, the id lookup will fail.
  1450. AZStd::erase_if(
  1451. m_registeredAreas,
  1452. [areaId, this](const auto& item)
  1453. {
  1454. auto const& [entityId, areaData] = item;
  1455. if (areaId == entityId)
  1456. {
  1457. m_dirtyRegion.AddAabb(areaData.m_areaBounds);
  1458. m_terrainDirtyMask |= AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask::HeightData |
  1459. AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask::SurfaceData;
  1460. if (ContainedAabbTouchesEdge(m_cachedAreaBounds, areaData.m_areaBounds))
  1461. {
  1462. RecalculateCachedBounds();
  1463. }
  1464. return true;
  1465. }
  1466. return false;
  1467. });
  1468. }
  1469. void TerrainSystem::RefreshArea(AZ::EntityId areaId, AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask changeMask)
  1470. {
  1471. AZStd::unique_lock<AZStd::shared_mutex> lock(m_areaMutex);
  1472. auto areaAabb = m_registeredAreas.find(areaId);
  1473. AZ::Aabb oldAabb = (areaAabb != m_registeredAreas.end()) ? areaAabb->second.m_areaBounds : AZ::Aabb::CreateNull();
  1474. AZ::Aabb newAabb = AZ::Aabb::CreateNull();
  1475. LmbrCentral::ShapeComponentRequestsBus::EventResult(newAabb, areaId, &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb);
  1476. m_registeredAreas[areaId].m_areaBounds = newAabb;
  1477. AZ::Aabb expandedAabb = oldAabb;
  1478. expandedAabb.AddAabb(newAabb);
  1479. RefreshRegion(expandedAabb, changeMask);
  1480. // Check to see which axis the aabbs changed in
  1481. bool xDiff = oldAabb.GetMin().GetX() != newAabb.GetMin().GetX() || oldAabb.GetMax().GetX() != newAabb.GetMax().GetX();
  1482. bool yDiff = oldAabb.GetMin().GetY() != newAabb.GetMin().GetY() || oldAabb.GetMax().GetY() != newAabb.GetMax().GetY();
  1483. bool zDiff = oldAabb.GetMin().GetZ() != newAabb.GetMin().GetZ() || oldAabb.GetMax().GetZ() != newAabb.GetMax().GetZ();
  1484. if ((xDiff && (m_cachedAreaBounds.GetMin().GetX() == oldAabb.GetMin().GetX() || m_cachedAreaBounds.GetMax().GetX() == oldAabb.GetMax().GetX())) ||
  1485. (yDiff && (m_cachedAreaBounds.GetMin().GetY() == oldAabb.GetMin().GetY() || m_cachedAreaBounds.GetMax().GetY() == oldAabb.GetMax().GetY())) ||
  1486. (zDiff && (m_cachedAreaBounds.GetMin().GetZ() == oldAabb.GetMin().GetZ() || m_cachedAreaBounds.GetMax().GetZ() == oldAabb.GetMax().GetZ())))
  1487. {
  1488. // Old aabb is on the edge of the bounds in at least one axis, and moved on that axis, so it will require a full refresh
  1489. RecalculateCachedBounds();
  1490. }
  1491. else if(!m_cachedAreaBounds.Contains(newAabb))
  1492. {
  1493. // Old Aabb was inside the bounds and new aabb is outside the bounds, so just add it.
  1494. m_cachedAreaBounds.AddAabb(newAabb);
  1495. }
  1496. }
  1497. void TerrainSystem::RefreshRegion(
  1498. const AZ::Aabb& dirtyRegion, AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask changeMask)
  1499. {
  1500. m_dirtyRegion.AddAabb(dirtyRegion);
  1501. // Keep track of which types of data have changed so that we can send out the appropriate notifications later.
  1502. m_terrainDirtyMask |= changeMask;
  1503. }
  1504. void TerrainSystem::OnTick(float /*deltaTime*/, AZ::ScriptTimePoint /*time*/)
  1505. {
  1506. using Terrain = AzFramework::Terrain::TerrainDataNotifications;
  1507. bool terrainSettingsChanged = false;
  1508. if ((m_terrainDirtyMask & AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask::Settings) ==
  1509. AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask::Settings)
  1510. {
  1511. terrainSettingsChanged = true;
  1512. m_terrainDirtyMask &= ~AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask::Settings;
  1513. if (m_currentSettings.m_heightRange.IsValid())
  1514. {
  1515. m_dirtyRegion = ClampZBoundsToHeightBounds(m_cachedAreaBounds);
  1516. }
  1517. // This needs to happen before the "system active" check below, because activating the system will cause the various
  1518. // terrain layer areas to request the current world bounds.
  1519. if (m_requestedSettings.m_heightRange != m_requestedSettings.m_heightRange)
  1520. {
  1521. m_terrainDirtyMask |= AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask::HeightData |
  1522. AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask::SurfaceData;
  1523. m_currentSettings.m_heightRange = m_requestedSettings.m_heightRange;
  1524. // Add the cached area bounds clamped to the new range, so both the old and new range are included.
  1525. m_dirtyRegion.AddAabb(ClampZBoundsToHeightBounds(m_cachedAreaBounds));
  1526. }
  1527. if (m_requestedSettings.m_heightQueryResolution != m_currentSettings.m_heightQueryResolution)
  1528. {
  1529. m_terrainDirtyMask |= AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask::HeightData;
  1530. }
  1531. if (m_requestedSettings.m_surfaceDataQueryResolution != m_currentSettings.m_surfaceDataQueryResolution)
  1532. {
  1533. m_terrainDirtyMask |= AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask::SurfaceData;
  1534. }
  1535. m_currentSettings = m_requestedSettings;
  1536. }
  1537. if (terrainSettingsChanged || (m_terrainDirtyMask != AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask::None))
  1538. {
  1539. Terrain::TerrainDataChangedMask changeMask = m_terrainDirtyMask;
  1540. if (terrainSettingsChanged)
  1541. {
  1542. changeMask |= Terrain::TerrainDataChangedMask::Settings;
  1543. }
  1544. // Make sure to set these *before* calling OnTerrainDataChanged, since it's possible that subsystems reacting to that call will
  1545. // cause the data to become dirty again.
  1546. AZ::Aabb dirtyRegion = ClampZBoundsToHeightBounds(m_dirtyRegion);
  1547. m_terrainDirtyMask = AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask::None;
  1548. m_dirtyRegion = AZ::Aabb::CreateNull();
  1549. AzFramework::Terrain::TerrainDataNotificationBus::Broadcast(
  1550. &AzFramework::Terrain::TerrainDataNotificationBus::Events::OnTerrainDataChanged, dirtyRegion,
  1551. changeMask);
  1552. }
  1553. }