TerrainRaycastContext.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  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 <TerrainRaycast/TerrainRaycastContext.h>
  9. #include <TerrainSystem/TerrainSystem.h>
  10. #include <AzCore/Math/IntersectSegment.h>
  11. #include <AzCore/std/sort.h>
  12. using namespace Terrain;
  13. namespace
  14. {
  15. ////////////////////////////////////////////////////////////////////////////////////////////////////
  16. // Convenience function to get the terrain height values at each corner of an AABB, triangulate them,
  17. // and then find the nearest intersection (if any) between the resulting triangles and the given ray.
  18. static void TriangulateAndFindNearestIntersection(const TerrainSystem& terrainSystem,
  19. const AZ::Aabb& aabb,
  20. const AZ::Intersect::SegmentTriangleHitTester& hitTester,
  21. AzFramework::RenderGeometry::RayResult& result)
  22. {
  23. // Obtain the height values at each corner of the AABB.
  24. const AZ::Vector3& aabbMin = aabb.GetMin();
  25. const AZ::Vector3& aabbMax = aabb.GetMax();
  26. AZ::Vector3 point0 = aabbMin;
  27. AZ::Vector3 point2 = aabbMax;
  28. AZ::Vector3 point1(point0.GetX(), point2.GetY(), 0.0f);
  29. AZ::Vector3 point3(point2.GetX(), point0.GetY(), 0.0f);
  30. point0.SetZ(terrainSystem.GetHeight(point0, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT));
  31. point1.SetZ(terrainSystem.GetHeight(point1, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT));
  32. point2.SetZ(terrainSystem.GetHeight(point2, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT));
  33. point3.SetZ(terrainSystem.GetHeight(point3, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT));
  34. // Finally, triangulate the four terrain points and check for a hit,
  35. // splitting using the top-left -> bottom-right diagonal so to match
  36. // the current behavior of the terrain physics and rendering systems.
  37. AZ::Vector3 bottomLeftHitNormal;
  38. float bottomLeftHitDistance;
  39. bool bottomLeftHit =
  40. hitTester.IntersectSegmentTriangleCCW(point0, point3, point1, bottomLeftHitNormal, bottomLeftHitDistance);
  41. AZ::Vector3 topRightHitNormal;
  42. float topRightHitDistance;
  43. bool topRightHit =
  44. hitTester.IntersectSegmentTriangleCCW(point2, point1, point3, topRightHitNormal, topRightHitDistance);
  45. if (bottomLeftHit && (!topRightHit || (bottomLeftHitDistance < topRightHitDistance)))
  46. {
  47. result.m_distance = bottomLeftHitDistance;
  48. result.m_worldNormal = bottomLeftHitNormal;
  49. result.m_worldPosition = hitTester.GetIntersectionPoint(result.m_distance);
  50. }
  51. else if (topRightHit)
  52. {
  53. result.m_distance = topRightHitDistance;
  54. result.m_worldNormal = topRightHitNormal;
  55. result.m_worldPosition = hitTester.GetIntersectionPoint(result.m_distance);
  56. }
  57. }
  58. } // namespace
  59. ////////////////////////////////////////////////////////////////////////////////////////////////////
  60. TerrainRaycastContext::TerrainRaycastContext(TerrainSystem& terrainSystem)
  61. : m_terrainSystem(terrainSystem)
  62. , m_entityContextId(AzFramework::EntityContextId::CreateRandom())
  63. {
  64. AzFramework::RenderGeometry::IntersectorBus::Handler::BusConnect(m_entityContextId);
  65. }
  66. ////////////////////////////////////////////////////////////////////////////////////////////////////
  67. TerrainRaycastContext::~TerrainRaycastContext()
  68. {
  69. AzFramework::RenderGeometry::IntersectorBus::Handler::BusDisconnect();
  70. }
  71. /*
  72. Iterative function that divides an AABB encompasing terrain points into grid squares based on
  73. the given grid resolution and steps along the ray visiting each voxel it intersects in order
  74. from nearest to farthest. In each square, it obtains the terrain height values at each corner and
  75. triangulates them to find the nearest intersection (if any) between the triangles and the ray.
  76. To step through the grid, we use an algorithm similar to Bresenham's line algorithm or a Digital
  77. Differential Analyzer. We can't use Bresenham's line algorithm itself because it will sometimes skip
  78. squares if the ray only passes through a tiny portion, and we need to use every square that it passes through.
  79. We start by clipping the ray itself to the terrain AABB so that we don't walk through any grid squares
  80. that cannot contain terrain. We then walk through the grid one square at a time, either moving horizontally
  81. or vertically to the next square based on the ray's slope, until we reach the end of the ray or we've found a hit.
  82. Visualization:
  83. - X: Grid square intersection but no triangle hit found
  84. - T: Grid square intersection with a triangle hit found
  85. ________________________________________
  86. | | | | | | | | |
  87. |____|____|____|____|____|____|____|____| Ray
  88. | | | | | | | | | /
  89. |____|____|____|____|____|____|____|____| /
  90. | | | | | | | | X |/
  91. |____|____|____|____|____|____|____|____/
  92. | | | | | | | | X /|
  93. |____|____|____|____|____|____|____|__/_|
  94. | | | | | | | | /X |
  95. |____|____|____|____|____|____|____|/___|
  96. | | | | | | | X / X |
  97. |____|____|____|____|____|____|___/|____|
  98. | | | | | | | T/ | |
  99. |____|____|____|____|____|____|____|____|
  100. | | | | | | | | |
  101. |____|____|____|____|____|____|____|____|
  102. */
  103. AzFramework::RenderGeometry::RayResult TerrainRaycastContext::RayIntersect(
  104. const AzFramework::RenderGeometry::RayRequest& ray)
  105. {
  106. const AZ::Aabb terrainWorldBounds = m_terrainSystem.GetTerrainAabb();
  107. const AZ::Vector2 terrainResolution(m_terrainSystem.GetTerrainHeightQueryResolution());
  108. // Initialize the result to invalid at the start.
  109. AzFramework::RenderGeometry::RayResult rayIntersectionResult = AzFramework::RenderGeometry::RayResult();
  110. if (!terrainWorldBounds.IsValid())
  111. {
  112. // There is no terrain to intersect.
  113. return rayIntersectionResult;
  114. }
  115. // Start by clipping the ray to the terrain world bounds so that we can reduce our iteration over the ray to just
  116. // the subset that can potentially collide with the terrain.
  117. // We use a slightly expanded terrain world bounds for clipping the ray so that precision errors don't cause the ray
  118. // to get overly truncated and miss a collision that might occur right on the world boundary.
  119. AZ::Vector3 clippedRayStart = ray.m_startWorldPosition;
  120. AZ::Vector3 clippedRayEnd = ray.m_endWorldPosition;
  121. float tClipStart, tClipEnd;
  122. bool rayIntersected = AZ::Intersect::ClipRayWithAabb(
  123. terrainWorldBounds.GetExpanded(AZ::Vector3(0.01f)), clippedRayStart, clippedRayEnd, tClipStart, tClipEnd);
  124. if (!rayIntersected)
  125. {
  126. // The ray does not intersect the terrain world bounds.
  127. return rayIntersectionResult;
  128. }
  129. // Move our clipped line segment into Vector2s for more convenient use below.
  130. const AZ::Vector2 clippedStart(clippedRayStart);
  131. const AZ::Vector2 clippedEnd(clippedRayEnd);
  132. const AZ::Vector2 clippedLineSegment = clippedEnd - clippedStart;
  133. // Calculate the total number of terrain squares we'll need to visit to trace the ray segment.
  134. // We need to visit 1 at the start, 1 for each X square we need to move, and 1 for each Y square we need to move,
  135. // since we'll always move either horizontally or vertically one square at a time when traversing the ray segment.
  136. const AZ::Vector2 numSquaresToMove =
  137. ((clippedEnd / terrainResolution).GetFloor() - (clippedStart / terrainResolution).GetFloor()).GetAbs();
  138. const int32_t numTerrainSquares =
  139. 1 + aznumeric_cast<int32_t>(numSquaresToMove.GetX()) + aznumeric_cast<int32_t>(numSquaresToMove.GetY());
  140. // This tells us how much t distance on the line to move to increment one terrain square in each direction.
  141. // Note that it could be infinity (due to a divide-by-0) if we're not moving in that direction.
  142. const AZ::Vector2 tDelta(terrainResolution / clippedLineSegment.GetAbs());
  143. // Get the min world space corner of the terrain grid square containing (x0, y0)
  144. const AZ::Vector2 clippedStartGridCorner = (clippedStart / terrainResolution).GetFloor() * terrainResolution;
  145. // tUntilNextBoundary stores how much further we currently need to move along t to get to the next terrain grid square boundary
  146. // in each direction.
  147. // We initialize with the fractional amount that we're starting in the square or max() if we're not moving in this
  148. // direction at all (when clippedLineSegment == 0)
  149. const AZ::Vector2 tFromMinCorner((clippedStart - clippedStartGridCorner) / clippedLineSegment.GetAbs());
  150. AZ::Vector2 tUntilNextBoundary = AZ::Vector2::CreateSelectCmpEqual(
  151. clippedLineSegment, AZ::Vector2::CreateZero(), AZ::Vector2(AZStd::numeric_limits<float>::max()), tFromMinCorner);
  152. // If we're moving in the positive direction in the square, then the amount till the next boundary is actually
  153. // the distance remaining to the max corner, not the distance in from the min corner, so flip our calculation.
  154. tUntilNextBoundary = AZ::Vector2::CreateSelectCmpGreater(clippedEnd, clippedStart, tDelta - tUntilNextBoundary, tUntilNextBoundary);
  155. // Initialize our segment/triangle hit tester with the ray that we're using. We use the full ray instead of the clipped one
  156. // to make sure we don't run into any precision issues caused from the clipping.
  157. AZ::Intersect::SegmentTriangleHitTester hitTester(ray.m_startWorldPosition, ray.m_endWorldPosition);
  158. // These will hold our current square coordinates in world space values as we loop through the squares, starting with the
  159. // grid square for (x0, y0). These values represent the minimum corner of each terrain square.
  160. AZ::Vector2 curGridCorner = clippedStartGridCorner;
  161. // This is how much we need to increment our x and y by to get to the next grid square along the line.
  162. // They will either be +/- terrainResolution or 0 if we're not moving in that direction.
  163. const AZ::Vector2 gridIncrement = terrainResolution *
  164. AZ::Vector2::CreateSelectCmpEqual(clippedLineSegment,
  165. AZ::Vector2::CreateZero(),
  166. AZ::Vector2::CreateZero(),
  167. AZ::Vector2(AZ::GetSign(clippedLineSegment.GetX()), AZ::GetSign(clippedLineSegment.GetY())));
  168. // Convenience vectors that we can use in the loop to just increment one direction.
  169. const AZ::Vector2 tDeltaX(tDelta.GetX(), 0.0f);
  170. const AZ::Vector2 tDeltaY(0.0f, tDelta.GetY());
  171. const AZ::Vector2 gridIncrementX(gridIncrement.GetX(), 0.0f);
  172. const AZ::Vector2 gridIncrementY(0.0f, gridIncrement.GetY());
  173. // Walk through each grid square in the terrain that intersects the XY coordinates of the line.
  174. // We'll check each square to see if the ray intersections actually intersect the terrain triangles in the square.
  175. for (int terrainSquare = 0; terrainSquare < numTerrainSquares; terrainSquare++)
  176. {
  177. // Create a bounding volume for this terrain square.
  178. AZ::Aabb currentVoxel = AZ::Aabb::CreateFromMinMax(
  179. AZ::Vector3(curGridCorner, terrainWorldBounds.GetMin().GetZ()),
  180. AZ::Vector3(curGridCorner + terrainResolution, terrainWorldBounds.GetMax().GetZ()));
  181. // Check for a hit against the terrain triangles in this square.
  182. // Note - this could be optimized to be 2x faster by adding some code to keep track of the terrain heights
  183. // from the previous square checked so that we only get the 2 new corners instead of all 4 every time.
  184. TriangulateAndFindNearestIntersection(m_terrainSystem, currentVoxel, hitTester, rayIntersectionResult);
  185. if (rayIntersectionResult)
  186. {
  187. // Intersection found. Replace the triangle normal from the hit with a higher-quality normal calculated
  188. // by the terrain system.
  189. rayIntersectionResult.m_worldNormal = m_terrainSystem.GetNormal(
  190. rayIntersectionResult.m_worldPosition, AzFramework::Terrain::TerrainDataRequests::Sampler::DEFAULT);
  191. // Return the distance in world space instead of in ray distance space.
  192. rayIntersectionResult.m_distance = rayIntersectionResult.m_worldPosition.GetDistance(ray.m_startWorldPosition);
  193. break;
  194. }
  195. // No hit yet, so move forward along the line (either horizontally or vertically) to the next terrain square.
  196. if (tUntilNextBoundary.GetY() < tUntilNextBoundary.GetX())
  197. {
  198. curGridCorner += gridIncrementY;
  199. tUntilNextBoundary += tDeltaY;
  200. }
  201. else
  202. {
  203. curGridCorner += gridIncrementX;
  204. tUntilNextBoundary += tDeltaX;
  205. }
  206. }
  207. // If needed we could call m_terrainSystem.FindBestAreaEntityAtPosition in order to set
  208. // rayIntersectionResult.m_entityAndComponent, but I'm not sure whether that is correct.
  209. return rayIntersectionResult;
  210. }