TerrainPhysicsColliderComponent.cpp 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
  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 <Components/TerrainPhysicsColliderComponent.h>
  9. #include <AzCore/Asset/AssetSerializer.h>
  10. #include <AzCore/Asset/AssetManagerBus.h>
  11. #include <AzCore/Component/Entity.h>
  12. #include <AzCore/Component/TransformBus.h>
  13. #include <AzCore/Casting/lossy_cast.h>
  14. #include <AzCore/Console/Console.h>
  15. #include <AzCore/Debug/Profiler.h>
  16. #include <AzCore/RTTI/BehaviorContext.h>
  17. #include <AzCore/Serialization/EditContext.h>
  18. #include <AzCore/Serialization/SerializeContext.h>
  19. #include <AzCore/std/parallel/binary_semaphore.h>
  20. #include <AzCore/std/smart_ptr/make_shared.h>
  21. #include <AzFramework/Terrain/TerrainDataRequestBus.h>
  22. #include <AzFramework/Physics/HeightfieldProviderBus.h>
  23. AZ_DECLARE_BUDGET(Terrain);
  24. namespace Terrain
  25. {
  26. AZ_CVAR(int32_t, cl_terrainPhysicsColliderMaxJobs, AzFramework::Terrain::QueryAsyncParams::UseMaxJobs, nullptr,
  27. AZ::ConsoleFunctorFlags::Null,
  28. "The maximum number of jobs to use when updating a Terrain Physics Collider (-1 will use all available cores).");
  29. Physics::HeightfieldProviderNotifications::HeightfieldChangeMask TerrainToPhysicsHeightfieldChangeMask(AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask mask)
  30. {
  31. using AzFramework::Terrain::TerrainDataNotifications;
  32. using Physics::HeightfieldProviderNotifications;
  33. HeightfieldProviderNotifications::HeightfieldChangeMask result = HeightfieldProviderNotifications::HeightfieldChangeMask::None;
  34. if ((mask & TerrainDataNotifications::TerrainDataChangedMask::Settings) == TerrainDataNotifications::TerrainDataChangedMask::Settings)
  35. {
  36. result |= HeightfieldProviderNotifications::HeightfieldChangeMask::Settings;
  37. }
  38. if ((mask & TerrainDataNotifications::TerrainDataChangedMask::HeightData) == TerrainDataNotifications::TerrainDataChangedMask::HeightData)
  39. {
  40. result |= HeightfieldProviderNotifications::HeightfieldChangeMask::HeightData;
  41. }
  42. if ((mask & TerrainDataNotifications::TerrainDataChangedMask::SurfaceData) == TerrainDataNotifications::TerrainDataChangedMask::SurfaceData)
  43. {
  44. result |= HeightfieldProviderNotifications::HeightfieldChangeMask::SurfaceData;
  45. }
  46. return result;
  47. }
  48. void TerrainPhysicsSurfaceMaterialMapping::Reflect(AZ::ReflectContext* context)
  49. {
  50. if (auto serialize = azrtti_cast<AZ::SerializeContext*>(context))
  51. {
  52. serialize->Class<TerrainPhysicsSurfaceMaterialMapping>()
  53. ->Version(4)
  54. ->Field("Surface", &TerrainPhysicsSurfaceMaterialMapping::m_surfaceTag)
  55. ->Field("MaterialAsset", &TerrainPhysicsSurfaceMaterialMapping::m_materialAsset)
  56. ;
  57. }
  58. }
  59. void TerrainPhysicsColliderConfig::Reflect(AZ::ReflectContext* context)
  60. {
  61. TerrainPhysicsSurfaceMaterialMapping::Reflect(context);
  62. if (auto serialize = azrtti_cast<AZ::SerializeContext*>(context))
  63. {
  64. serialize->Class<TerrainPhysicsColliderConfig>()
  65. ->Version(5)
  66. ->Field("DefaultMaterialAsset", &TerrainPhysicsColliderConfig::m_defaultMaterialAsset)
  67. ->Field("Mappings", &TerrainPhysicsColliderConfig::m_surfaceMaterialMappings)
  68. ;
  69. }
  70. }
  71. void TerrainPhysicsColliderComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services)
  72. {
  73. services.push_back(AZ_CRC_CE("PhysicsHeightfieldProviderService"));
  74. }
  75. void TerrainPhysicsColliderComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& services)
  76. {
  77. services.push_back(AZ_CRC_CE("PhysicsHeightfieldProviderService"));
  78. }
  79. void TerrainPhysicsColliderComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& services)
  80. {
  81. services.push_back(AZ_CRC_CE("AxisAlignedBoxShapeService"));
  82. }
  83. void TerrainPhysicsColliderComponent::GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& services)
  84. {
  85. // If any of the following appear on the same entity as this one, they should get activated first as their data will
  86. // affect this component.
  87. services.push_back(AZ_CRC_CE("TerrainAreaService"));
  88. services.push_back(AZ_CRC_CE("TerrainHeightProviderService"));
  89. services.push_back(AZ_CRC_CE("TerrainSurfaceProviderService"));
  90. }
  91. void TerrainPhysicsColliderComponent::Reflect(AZ::ReflectContext* context)
  92. {
  93. TerrainPhysicsColliderConfig::Reflect(context);
  94. if (auto serialize = azrtti_cast<AZ::SerializeContext*>(context))
  95. {
  96. serialize->Class<TerrainPhysicsColliderComponent, AZ::Component>()
  97. ->Version(0)
  98. ->Field("Configuration", &TerrainPhysicsColliderComponent::m_configuration)
  99. ;
  100. }
  101. }
  102. TerrainPhysicsColliderComponent::TerrainPhysicsColliderComponent(const TerrainPhysicsColliderConfig& configuration)
  103. : m_configuration(configuration)
  104. {
  105. }
  106. TerrainPhysicsColliderComponent::TerrainPhysicsColliderComponent()
  107. {
  108. }
  109. void TerrainPhysicsColliderComponent::Activate()
  110. {
  111. // Build a mapping of surface tags to material indices for quick lookups when building/refreshing the collider.
  112. BuildSurfaceTagToMaterialIndexLookup();
  113. const auto entityId = GetEntityId();
  114. LmbrCentral::ShapeComponentNotificationsBus::Handler::BusConnect(entityId);
  115. Physics::HeightfieldProviderRequestsBus::Handler::BusConnect(entityId);
  116. AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusConnect();
  117. }
  118. void TerrainPhysicsColliderComponent::Deactivate()
  119. {
  120. AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusDisconnect();
  121. Physics::HeightfieldProviderRequestsBus::Handler ::BusDisconnect();
  122. LmbrCentral::ShapeComponentNotificationsBus::Handler::BusDisconnect();
  123. }
  124. void TerrainPhysicsColliderComponent::NotifyListenersOfHeightfieldDataChange(
  125. const Physics::HeightfieldProviderNotifications::HeightfieldChangeMask heightfieldChangeMask,
  126. const AZ::Aabb& dirtyRegion)
  127. {
  128. AZ_PROFILE_FUNCTION(Terrain);
  129. CalculateHeightfieldRegion();
  130. AZ::Aabb colliderBounds = GetHeightfieldAabb();
  131. if (dirtyRegion.IsValid())
  132. {
  133. // If we have a dirty region, only update this collider if the dirty region overlaps the collider bounds.
  134. if (dirtyRegion.Overlaps(colliderBounds))
  135. {
  136. // Find the intersection of the dirty region and the collider, and only notify about that area as changing.
  137. AZ::Aabb dirtyBounds = colliderBounds.GetClamped(dirtyRegion);
  138. Physics::HeightfieldProviderNotificationBus::Event(
  139. GetEntityId(),
  140. &Physics::HeightfieldProviderNotificationBus::Events::OnHeightfieldDataChanged, dirtyBounds, heightfieldChangeMask);
  141. }
  142. }
  143. else
  144. {
  145. // No valid dirty region, so update the entire collider bounds.
  146. Physics::HeightfieldProviderNotificationBus::Event(
  147. GetEntityId(),
  148. &Physics::HeightfieldProviderNotificationBus::Events::OnHeightfieldDataChanged, colliderBounds, heightfieldChangeMask);
  149. }
  150. }
  151. void TerrainPhysicsColliderComponent::OnShapeChanged([[maybe_unused]] ShapeChangeReasons changeReason)
  152. {
  153. // This will notify us of both shape changes and transform changes.
  154. // It's important to use this event for transform changes instead of listening to OnTransformChanged, because we need to guarantee
  155. // the shape has received the transform change message and updated its internal state before passing it along to us.
  156. Physics::HeightfieldProviderNotifications::HeightfieldChangeMask changeMask =
  157. Physics::HeightfieldProviderNotifications::HeightfieldChangeMask::Settings |
  158. Physics::HeightfieldProviderNotifications::HeightfieldChangeMask::HeightData;
  159. NotifyListenersOfHeightfieldDataChange(changeMask, AZ::Aabb::CreateNull());
  160. }
  161. void TerrainPhysicsColliderComponent::OnTerrainDataCreateEnd()
  162. {
  163. m_terrainDataActive = true;
  164. // The terrain system has finished creating itself, so we should now have data for creating a heightfield.
  165. // Notify this as a 'settings' change because the heightfield has changed activation status.
  166. NotifyListenersOfHeightfieldDataChange(
  167. Physics::HeightfieldProviderNotifications::HeightfieldChangeMask::Settings, AZ::Aabb::CreateNull());
  168. }
  169. void TerrainPhysicsColliderComponent::OnTerrainDataDestroyBegin()
  170. {
  171. m_terrainDataActive = false;
  172. // The terrain system is starting to destroy itself, so notify listeners of a change since the heightfield
  173. // will no longer have any valid data.
  174. // Notify this as a 'settings' change because the heightfield has changed activation status.
  175. NotifyListenersOfHeightfieldDataChange(
  176. Physics::HeightfieldProviderNotifications::HeightfieldChangeMask::Settings, AZ::Aabb::CreateNull());
  177. }
  178. void TerrainPhysicsColliderComponent::OnTerrainDataChanged(
  179. const AZ::Aabb& dirtyRegion, TerrainDataChangedMask dataChangedMask)
  180. {
  181. if (m_terrainDataActive)
  182. {
  183. Physics::HeightfieldProviderNotifications::HeightfieldChangeMask physicsMask =
  184. TerrainToPhysicsHeightfieldChangeMask(dataChangedMask);
  185. NotifyListenersOfHeightfieldDataChange(physicsMask, dirtyRegion);
  186. }
  187. }
  188. void TerrainPhysicsColliderComponent::CalculateHeightfieldRegion()
  189. {
  190. if (!m_terrainDataActive)
  191. {
  192. AZStd::unique_lock lock(m_stateMutex);
  193. m_heightfieldRegion = AzFramework::Terrain::TerrainQueryRegion();
  194. return;
  195. }
  196. AZ::Aabb heightfieldBox = AZ::Aabb::CreateNull();
  197. LmbrCentral::ShapeComponentRequestsBus::EventResult(
  198. heightfieldBox, GetEntityId(), &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb);
  199. const AZ::Vector2 gridResolution = GetHeightfieldGridSpacing();
  200. AZ::Vector2 constrictedAlignedStartPoint = (AZ::Vector2(heightfieldBox.GetMin()) / gridResolution).GetCeil() * gridResolution;
  201. AZ::Vector2 constrictedAlignedEndPoint = (AZ::Vector2(heightfieldBox.GetMax()) / gridResolution).GetFloor() * gridResolution;
  202. // The "+ 1.0" at the end is because we need to be sure to include the end points. (ex: start=1, end=4 should have 4 points)
  203. AZ::Vector2 numPoints = (constrictedAlignedEndPoint - constrictedAlignedStartPoint) / gridResolution + AZ::Vector2(1.0f);
  204. {
  205. AZStd::unique_lock lock(m_stateMutex);
  206. m_heightfieldRegion.m_startPoint =
  207. AZ::Vector3(constrictedAlignedStartPoint.GetX(), constrictedAlignedStartPoint.GetY(), heightfieldBox.GetMin().GetZ());
  208. m_heightfieldRegion.m_stepSize = gridResolution;
  209. m_heightfieldRegion.m_numPointsX = aznumeric_cast<size_t>(numPoints.GetX());
  210. m_heightfieldRegion.m_numPointsY = aznumeric_cast<size_t>(numPoints.GetY());
  211. }
  212. }
  213. AZ::Aabb TerrainPhysicsColliderComponent::GetHeightfieldAabb() const
  214. {
  215. if (!m_terrainDataActive)
  216. {
  217. return AZ::Aabb::CreateNull();
  218. }
  219. AZ::Aabb heightfieldBox = AZ::Aabb::CreateNull();
  220. LmbrCentral::ShapeComponentRequestsBus::EventResult(
  221. heightfieldBox, GetEntityId(), &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb);
  222. {
  223. AZStd::shared_lock lock(m_stateMutex);
  224. AZ::Vector3 endPoint = m_heightfieldRegion.m_startPoint +
  225. AZ::Vector3(m_heightfieldRegion.m_stepSize.GetX() * (m_heightfieldRegion.m_numPointsX - 1),
  226. m_heightfieldRegion.m_stepSize.GetY() * (m_heightfieldRegion.m_numPointsY - 1), heightfieldBox.GetZExtent());
  227. return AZ::Aabb::CreateFromMinMax(m_heightfieldRegion.m_startPoint, endPoint);
  228. }
  229. }
  230. void TerrainPhysicsColliderComponent::GetHeightfieldHeightBounds(float& minHeightBounds, float& maxHeightBounds) const
  231. {
  232. if (!m_terrainDataActive)
  233. {
  234. minHeightBounds = 0.0f;
  235. maxHeightBounds = 0.0f;
  236. return;
  237. }
  238. const AZ::Aabb heightfieldAabb = GetHeightfieldAabb();
  239. // Because our terrain heights are relative to the center of the bounding box, the min and max allowable heights are also
  240. // relative to the center. They are also clamped to the size of the bounding box.
  241. maxHeightBounds = heightfieldAabb.GetZExtent() / 2.0f;
  242. minHeightBounds = -maxHeightBounds;
  243. }
  244. float TerrainPhysicsColliderComponent::GetHeightfieldMinHeight() const
  245. {
  246. float minHeightBounds{ 0.0f };
  247. float maxHeightBounds{ 0.0f };
  248. GetHeightfieldHeightBounds(minHeightBounds, maxHeightBounds);
  249. return minHeightBounds;
  250. }
  251. float TerrainPhysicsColliderComponent::GetHeightfieldMaxHeight() const
  252. {
  253. float minHeightBounds{ 0.0f };
  254. float maxHeightBounds{ 0.0f };
  255. GetHeightfieldHeightBounds(minHeightBounds, maxHeightBounds);
  256. return maxHeightBounds;
  257. }
  258. AZ::Transform TerrainPhysicsColliderComponent::GetHeightfieldTransform() const
  259. {
  260. // We currently don't support rotation of terrain heightfields.
  261. // We also need to adjust the center to account for the fact that the heightfield might be expanded unevenly from
  262. // the entity's center, depending on where the entity's shape lies relative to the terrain grid.
  263. return AZ::Transform::CreateTranslation(GetHeightfieldAabb().GetCenter());
  264. }
  265. void TerrainPhysicsColliderComponent::GenerateHeightsInBounds(AZStd::vector<float>& heights) const
  266. {
  267. AZ_PROFILE_FUNCTION(Terrain);
  268. AzFramework::Terrain::TerrainQueryRegion queryRegion;
  269. {
  270. AZStd::shared_lock lock(m_stateMutex);
  271. queryRegion = m_heightfieldRegion;
  272. }
  273. heights.clear();
  274. heights.reserve(queryRegion.m_numPointsX * queryRegion.m_numPointsY);
  275. AZ::Aabb worldSize = GetHeightfieldAabb();
  276. const float worldCenterZ = worldSize.GetCenter().GetZ();
  277. auto perPositionHeightCallback = [&heights, worldCenterZ]
  278. ([[maybe_unused]] size_t xIndex, [[maybe_unused]] size_t yIndex, const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists)
  279. {
  280. heights.emplace_back(surfacePoint.m_position.GetZ() - worldCenterZ);
  281. };
  282. // We can use the "EXACT" sampler here because our query points are guaranteed to be aligned with terrain grid points.
  283. AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
  284. &AzFramework::Terrain::TerrainDataRequests::QueryRegion, queryRegion,
  285. AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Heights,
  286. perPositionHeightCallback, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT);
  287. }
  288. uint8_t TerrainPhysicsColliderComponent::GetMaterialIndex(
  289. const AZ::Data::Asset<Physics::MaterialAsset>& materialAsset,
  290. const AZStd::vector<AZ::Data::Asset<Physics::MaterialAsset>>& materialList) const
  291. {
  292. const auto& materialIter = AZStd::find(materialList.begin(), materialList.end(), materialAsset);
  293. if (materialIter != materialList.end())
  294. {
  295. return static_cast<uint8_t>(materialIter - materialList.begin());
  296. }
  297. return DefaultMaterialIndex;
  298. }
  299. AZ::Data::Asset<Physics::MaterialAsset> TerrainPhysicsColliderComponent::FindMaterialAssetForSurfaceTag(const SurfaceData::SurfaceTag tag) const
  300. {
  301. AZStd::shared_lock lock(m_stateMutex);
  302. for (auto& mapping : m_configuration.m_surfaceMaterialMappings)
  303. {
  304. if (mapping.m_surfaceTag == tag)
  305. {
  306. return mapping.m_materialAsset;
  307. }
  308. }
  309. // If this surface isn't mapped, use the default material.
  310. return m_configuration.m_defaultMaterialAsset;
  311. }
  312. void TerrainPhysicsColliderComponent::GetHeightfieldIndicesFromRegion(
  313. const AZ::Aabb& regionIn, size_t& startColumn, size_t& startRow, size_t& numColumns, size_t& numRows) const
  314. {
  315. if (!m_terrainDataActive)
  316. {
  317. startRow = 0;
  318. startColumn = 0;
  319. numRows = 0;
  320. numColumns = 0;
  321. return;
  322. }
  323. AZ::Aabb region = regionIn;
  324. AZ::Aabb worldSize = GetHeightfieldAabb();
  325. if (!region.IsValid())
  326. {
  327. region = worldSize;
  328. }
  329. else
  330. {
  331. region.Clamp(worldSize);
  332. }
  333. const AZ::Vector2 gridResolution = GetHeightfieldGridSpacing();
  334. size_t xOffset, yOffset;
  335. {
  336. AZStd::shared_lock lock(m_stateMutex);
  337. // Convert the heightfield start point from world scale (1 = 1 meter) to terrain grid scale (1 = 1 terrain square)
  338. AZ::Vector2 heightfieldStartGridPoint = AZ::Vector2(m_heightfieldRegion.m_startPoint) / m_heightfieldRegion.m_stepSize;
  339. AZ::Vector2 contractedAlignedStartGridPoint = (AZ::Vector2(region.GetMin()) / gridResolution).GetCeil();
  340. AZ::Vector2 contractedAlignedEndGridPoint = (AZ::Vector2(region.GetMax()) / gridResolution).GetFloor();
  341. xOffset = aznumeric_cast<size_t>(contractedAlignedStartGridPoint.GetX() - heightfieldStartGridPoint.GetX());
  342. yOffset = aznumeric_cast<size_t>(contractedAlignedStartGridPoint.GetY() - heightfieldStartGridPoint.GetY());
  343. // The "+ 1.0" at the end is because we need to be sure to include the end points. (ex: start=1, end=4 should have 4 points)
  344. AZ::Vector2 numPoints = contractedAlignedEndGridPoint - contractedAlignedStartGridPoint + AZ::Vector2(1.0f);
  345. const size_t numPointsX = AZStd::min(aznumeric_cast<size_t>(numPoints.GetX()), m_heightfieldRegion.m_numPointsX);
  346. const size_t numPointsY = AZStd::min(aznumeric_cast<size_t>(numPoints.GetY()), m_heightfieldRegion.m_numPointsY);
  347. startColumn = xOffset;
  348. startRow = yOffset;
  349. numColumns = numPointsX;
  350. numRows = numPointsY;
  351. }
  352. }
  353. //! Updates the list of heights and materials within the region.
  354. void TerrainPhysicsColliderComponent::UpdateHeightsAndMaterialsAsync(
  355. const Physics::UpdateHeightfieldSampleFunction& updateHeightsMaterialsCallback,
  356. const Physics::UpdateHeightfieldCompleteFunction& updateHeightsCompleteCallback,
  357. size_t startColumn,
  358. size_t startRow,
  359. size_t numColumns,
  360. size_t numRows) const
  361. {
  362. using namespace AzFramework::Terrain;
  363. AZ_PROFILE_FUNCTION(Terrain);
  364. // Early out if there's no terrain data, or we aren't trying to update any points.
  365. if ((!m_terrainDataActive) || (numColumns == 0) || (numRows == 0))
  366. {
  367. updateHeightsCompleteCallback();
  368. return;
  369. }
  370. AZ::Aabb worldSize = GetHeightfieldAabb();
  371. const AZ::Vector2 gridResolution = GetHeightfieldGridSpacing();
  372. AZ::Vector2 startPoint =
  373. AZ::Vector2(worldSize.GetMin())
  374. + (AZ::Vector2(aznumeric_cast<float>(startColumn), aznumeric_cast<float>(startRow)) * gridResolution);
  375. TerrainQueryRegion queryRegion = TerrainQueryRegion(startPoint, numColumns, numRows, gridResolution);
  376. const float worldCenterZ = worldSize.GetCenter().GetZ();
  377. const float worldHeightBoundsMin = worldSize.GetMin().GetZ();
  378. const float worldHeightBoundsMax = worldSize.GetMax().GetZ();
  379. // Grab a local copy of the surface tag to material lookup to ensure that modifications on other threads
  380. // don't affect us while we're in the middle of the query.
  381. AZStd::unordered_map<SurfaceData::SurfaceTag, uint8_t> surfaceTagToMaterialIndexLookup;
  382. {
  383. AZStd::shared_lock lock(m_stateMutex);
  384. surfaceTagToMaterialIndexLookup = m_surfaceTagToMaterialIndexLookup;
  385. }
  386. // Everything is copied by value into the lambda because this is an async callback, so anything referenced by it needs to
  387. // continue to exist after the outer function completes.
  388. auto perPositionCallback =
  389. [startColumn, startRow, updateHeightsMaterialsCallback, surfaceTagToMaterialIndexLookup, worldCenterZ,
  390. worldHeightBoundsMin, worldHeightBoundsMax]
  391. (size_t xIndex, size_t yIndex, const AzFramework::SurfaceData::SurfacePoint& surfacePoint, bool terrainExists)
  392. {
  393. float height = surfacePoint.m_position.GetZ();
  394. // Any heights that fall outside the range of our bounding box will get turned into holes.
  395. if ((height < worldHeightBoundsMin) || (height > worldHeightBoundsMax))
  396. {
  397. height = worldHeightBoundsMin;
  398. terrainExists = false;
  399. }
  400. // Find the best surface tag at this point.
  401. // We want the MaxSurfaceWeight. The ProcessSurfacePoints callback has surface weights sorted.
  402. // So, we pick the value at the front of the list.
  403. AzFramework::SurfaceData::SurfaceTagWeight surfaceWeight;
  404. if (!surfacePoint.m_surfaceTags.empty())
  405. {
  406. surfaceWeight = *surfacePoint.m_surfaceTags.begin();
  407. }
  408. Physics::HeightMaterialPoint point;
  409. point.m_height = height - worldCenterZ;
  410. point.m_quadMeshType = terrainExists ? Physics::QuadMeshType::SubdivideUpperLeftToBottomRight : Physics::QuadMeshType::Hole;
  411. // Get the material index for the surface type. If we can't find it, use the default material.
  412. if (const auto& entry = surfaceTagToMaterialIndexLookup.find(surfaceWeight.m_surfaceType);
  413. entry != surfaceTagToMaterialIndexLookup.end())
  414. {
  415. point.m_materialIndex = entry->second;
  416. }
  417. else
  418. {
  419. point.m_materialIndex = DefaultMaterialIndex;
  420. }
  421. size_t column = startColumn + xIndex;
  422. size_t row = startRow + yIndex;
  423. updateHeightsMaterialsCallback(column, row, point);
  424. };
  425. // Create an async query to update all of the height and material data so that we can spread the computation across
  426. // multiple threads and then call back a completion method at the end.
  427. AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> jobContext;
  428. auto params = AZStd::make_shared<AzFramework::Terrain::QueryAsyncParams>();
  429. params->m_desiredNumberOfJobs = cl_terrainPhysicsColliderMaxJobs;
  430. params->m_completionCallback =
  431. [updateHeightsCompleteCallback]([[maybe_unused]] AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> context)
  432. {
  433. updateHeightsCompleteCallback();
  434. };
  435. // We can use the "EXACT" sampler here because our query points are guaranteed to be aligned with terrain grid points.
  436. AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
  437. jobContext, &AzFramework::Terrain::TerrainDataRequests::QueryRegionAsync, queryRegion,
  438. static_cast<TerrainDataRequests::TerrainDataMask>(
  439. TerrainDataRequests::TerrainDataMask::Heights | TerrainDataRequests::TerrainDataMask::SurfaceData),
  440. perPositionCallback, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, params);
  441. // If the call to UpdateHeightsAndMaterials was made on a thread, and the TerrainSystem is currently shutting down on a different
  442. // thread, it's possible that the TerrainDataRequest bus won't have a listener at the moment we call it, which is why we need
  443. // to validate that the jobContext was returned successfully. If it wasn't, just call the completion callback immediately.
  444. if (!jobContext)
  445. {
  446. updateHeightsCompleteCallback();
  447. }
  448. }
  449. //! Updates the list of heights and materials within the region.
  450. void TerrainPhysicsColliderComponent::UpdateHeightsAndMaterials(
  451. const Physics::UpdateHeightfieldSampleFunction& updateHeightsMaterialsCallback,
  452. size_t startColumn,
  453. size_t startRow,
  454. size_t numColumns,
  455. size_t numRows) const
  456. {
  457. AZ_PROFILE_FUNCTION(Terrain);
  458. AZStd::binary_semaphore wait;
  459. auto completionCallback = [&wait]()
  460. {
  461. wait.release();
  462. };
  463. UpdateHeightsAndMaterialsAsync(updateHeightsMaterialsCallback, completionCallback, startColumn, startRow, numColumns, numRows);
  464. // Wait for the query to complete.
  465. wait.acquire();
  466. }
  467. void TerrainPhysicsColliderComponent::BuildSurfaceTagToMaterialIndexLookup()
  468. {
  469. auto materialList = GetMaterialList();
  470. // Lock this *after* calling GetMaterialList() so that we don't have nested locks.
  471. AZStd::unique_lock lock(m_stateMutex);
  472. m_surfaceTagToMaterialIndexLookup.clear();
  473. for (const auto& mapping : m_configuration.m_surfaceMaterialMappings)
  474. {
  475. for (uint8_t materialIndex = 0; materialIndex < materialList.size(); materialIndex++)
  476. {
  477. if (mapping.m_materialAsset == materialList[materialIndex])
  478. {
  479. m_surfaceTagToMaterialIndexLookup.emplace(mapping.m_surfaceTag, materialIndex);
  480. break;
  481. }
  482. }
  483. }
  484. }
  485. void TerrainPhysicsColliderComponent::UpdateConfiguration(const TerrainPhysicsColliderConfig& newConfiguration)
  486. {
  487. {
  488. AZStd::unique_lock lock(m_stateMutex);
  489. m_configuration = newConfiguration;
  490. }
  491. // Build a mapping of surface tags to material indices for quick lookups when building/refreshing the collider.
  492. BuildSurfaceTagToMaterialIndexLookup();
  493. NotifyListenersOfHeightfieldDataChange(
  494. Physics::HeightfieldProviderNotifications::HeightfieldChangeMask::SurfaceMapping, AZ::Aabb::CreateNull());
  495. }
  496. AZ::Vector2 TerrainPhysicsColliderComponent::GetHeightfieldGridSpacing() const
  497. {
  498. if (!m_terrainDataActive)
  499. {
  500. return AZ::Vector2(0.0f);
  501. }
  502. float gridResolution = 1.0f;
  503. AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
  504. gridResolution, &AzFramework::Terrain::TerrainDataRequests::GetTerrainHeightQueryResolution);
  505. return AZ::Vector2(gridResolution);
  506. }
  507. void TerrainPhysicsColliderComponent::GetHeightfieldGridSize(size_t& numColumns, size_t& numRows) const
  508. {
  509. AZStd::shared_lock lock(m_stateMutex);
  510. numColumns = m_heightfieldRegion.m_numPointsX;
  511. numRows = m_heightfieldRegion.m_numPointsY;
  512. }
  513. AZ::u64 TerrainPhysicsColliderComponent::GetHeightfieldGridColumns() const
  514. {
  515. AZStd::shared_lock lock(m_stateMutex);
  516. return static_cast<AZ::u64>(m_heightfieldRegion.m_numPointsX);
  517. }
  518. AZ::u64 TerrainPhysicsColliderComponent::GetHeightfieldGridRows() const
  519. {
  520. AZStd::shared_lock lock(m_stateMutex);
  521. return static_cast<AZ::u64>(m_heightfieldRegion.m_numPointsY);
  522. }
  523. AZStd::vector<AZ::Data::Asset<Physics::MaterialAsset>> TerrainPhysicsColliderComponent::GetMaterialList() const
  524. {
  525. AZStd::shared_lock lock(m_stateMutex);
  526. AZStd::vector<AZ::Data::Asset<Physics::MaterialAsset>> materialList;
  527. materialList.reserve(m_configuration.m_surfaceMaterialMappings.size() + 1); // +1 for default material asset
  528. // Ensure the list contains the default material as the first entry.
  529. materialList.push_back(m_configuration.m_defaultMaterialAsset);
  530. for (auto& mapping : m_configuration.m_surfaceMaterialMappings)
  531. {
  532. const auto& existingInstance = AZStd::find(materialList.begin(), materialList.end(), mapping.m_materialAsset);
  533. if (existingInstance == materialList.end()) // Avoid having the same asset more than once
  534. {
  535. materialList.push_back(mapping.m_materialAsset);
  536. }
  537. }
  538. return materialList;
  539. }
  540. AZStd::vector<float> TerrainPhysicsColliderComponent::GetHeights() const
  541. {
  542. AZStd::vector<float> heights;
  543. GenerateHeightsInBounds(heights);
  544. return heights;
  545. }
  546. AZStd::vector<Physics::HeightMaterialPoint> TerrainPhysicsColliderComponent::GetHeightsAndMaterials() const
  547. {
  548. size_t gridWidth = 0, gridHeight = 0;
  549. GetHeightfieldGridSize(gridWidth, gridHeight);
  550. AZ_Assert(gridWidth * gridHeight != 0, "GetHeightsAndMaterials: Invalid grid size. Size cannot be zero.");
  551. AZStd::vector<Physics::HeightMaterialPoint> heightMaterials(gridWidth * gridHeight);
  552. UpdateHeightsAndMaterials(
  553. [&heightMaterials, gridWidth](size_t col, size_t row, const Physics::HeightMaterialPoint& point)
  554. {
  555. heightMaterials[col + row * gridWidth] = point;
  556. },
  557. 0, 0, gridWidth, gridHeight);
  558. return heightMaterials;
  559. }
  560. }