2
0
Эх сурвалжийг харах

[Terrain] Replace Process* APIs with simplified Query* APIs and fix bugs discovered while testing (#9748)

* First pass at reducing the Process* APIs.
* Replaced all the variants with single QueryList / QueryRegion APIs that take in a flag that describes what data to query for
* Changed QueryRegion to take in a new TerrainQueryRegion struct that desribes start point, step size, and number of points to query
* Fixed bug in QueryRegionAsync (ProcessRegionAsync) where the x and y indices passed to subregion per-position callbacks were incorrect
* Reworked TerrainWorldDebugger to make the logic more straightforward, threadsafe, and respect the terrainExists flag so that it won't draw wireframes outside of terrain spawners

Signed-off-by: Mike Balfour <[email protected]>

* Fix physics component ordering and dirty region notifications.
Try to reduce the number of update calls that occur by only updating terrain physics components that overlap the dirty region.
Also, make sure the components start up in the correct order to help reduce the number of redundant notifications that can happen.

Signed-off-by: Mike Balfour <[email protected]>

* Better explanation of logic choice

Signed-off-by: Mike Balfour <[email protected]>

* Fixed multiple bugs in Terrain physics components.
* Wireframe rendering wasn't drawing in the same location as the actual physics heightfield
* Unit tests were creating semi-invalid heightfield game entities and checking invalid locations to validate them
* The heightfield was treating the last set of vertices inconsistently as either the last vertices or the start of the last quads, leading to bad geometry
* The heightfield offset was created incorrectly due to those inconsistencies
* Heightfield was expanding beyond the bounds to create the heightfield aligned ot the grid. This originally seemed like a good idea, but it doesn't match the rest of the terrain system, so this is switched to constrict to be inside the bounds now instead.

Also started adding some detection for when the component shares an entity with a terrain layer spawner, to avoid some redundant updates that can occur.

Signed-off-by: Mike Balfour <[email protected]>

* Fixed up terrain physics collider unit tests to account for new "endcap" data in the heightfield.

Signed-off-by: Mike Balfour <[email protected]>

* Code cleanups

Signed-off-by: Mike Balfour <[email protected]>

* Small cleanups related to samplers

Signed-off-by: Mike Balfour <[email protected]>

* Reduced the total amount of data being requested by physics

Signed-off-by: Mike Balfour <[email protected]>

* Fixed up automated tests for terrain physics collider

Signed-off-by: Mike Balfour <[email protected]>

* Removed detection of Terrain Layer Spawner, ultimately this wasn't effective.

Signed-off-by: Mike Balfour <[email protected]>

* Fixed comment

Signed-off-by: Mike Balfour <[email protected]>

* Small cleanup of dead code

Signed-off-by: Mike Balfour <[email protected]>

* Addressed PR feedback

Signed-off-by: Mike Balfour <[email protected]>

* Fixed bug with terrain detail manager query.
Also changed default on drawing the AABB around the physics heightfield query. Most people shouldn't need to see that.

Signed-off-by: Mike Balfour <[email protected]>
Mike Balfour 3 жил өмнө
parent
commit
2c966ccac8
28 өөрчлөгдсөн 1092 нэмэгдсэн , 1305 устгасан
  1. 9 3
      AutomatedTesting/Gem/PythonTests/Terrain/EditorScripts/TerrainPhysicsCollider_ChangesSizeWithAxisAlignedBoxShapeChanges.py
  2. 14 16
      AutomatedTesting/Levels/Terrain/TerrainPhysicsCollider_MaterialMapping_Works/TerrainPhysicsCollider_MaterialMapping_Works.prefab
  3. 1 17
      Code/Editor/GameEngine.cpp
  4. 16 4
      Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.cpp
  5. 6 4
      Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.h
  6. 41 0
      Code/Framework/AzFramework/AzFramework/Terrain/TerrainDataRequestBus.cpp
  7. 131 164
      Code/Framework/AzFramework/AzFramework/Terrain/TerrainDataRequestBus.h
  8. 31 45
      Code/Framework/AzFramework/Tests/Mocks/Terrain/MockTerrainDataRequestBus.h
  9. 22 9
      Gems/PhysX/Code/Source/EditorHeightfieldColliderComponent.cpp
  10. 2 5
      Gems/PhysX/Code/Source/HeightfieldColliderComponent.cpp
  11. 7 7
      Gems/PhysX/Code/Source/Utils.cpp
  12. 28 11
      Gems/PhysX/Code/Tests/EditorHeightfieldColliderComponentTests.cpp
  13. 89 79
      Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.cpp
  14. 5 1
      Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.h
  15. 3 2
      Gems/Terrain/Code/Source/Components/TerrainSurfaceDataSystemComponent.cpp
  16. 2 0
      Gems/Terrain/Code/Source/Components/TerrainWorldComponent.cpp
  17. 84 70
      Gems/Terrain/Code/Source/Components/TerrainWorldDebuggerComponent.cpp
  18. 3 3
      Gems/Terrain/Code/Source/Components/TerrainWorldDebuggerComponent.h
  19. 9 0
      Gems/Terrain/Code/Source/EditorComponents/EditorTerrainPhysicsColliderComponent.cpp
  20. 1 0
      Gems/Terrain/Code/Source/EditorComponents/EditorTerrainPhysicsColliderComponent.h
  21. 4 2
      Gems/Terrain/Code/Source/TerrainRenderer/TerrainDetailMaterialManager.cpp
  22. 12 24
      Gems/Terrain/Code/Source/TerrainRenderer/TerrainMeshManager.cpp
  23. 211 405
      Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp
  24. 53 249
      Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h
  25. 59 37
      Gems/Terrain/Code/Tests/TerrainBulkQueryTests.cpp
  26. 115 70
      Gems/Terrain/Code/Tests/TerrainPhysicsColliderTests.cpp
  27. 100 63
      Gems/Terrain/Code/Tests/TerrainSystemBenchmarks.cpp
  28. 34 15
      Gems/Terrain/Code/Tests/TerrainSystemTest.cpp

+ 9 - 3
AutomatedTesting/Gem/PythonTests/Terrain/EditorScripts/TerrainPhysicsCollider_ChangesSizeWithAxisAlignedBoxShapeChanges.py

@@ -51,14 +51,20 @@ def TerrainPhysicsCollider_ChangesSizeWithAxisAlignedBoxShapeChanges():
 
     SET_BOX_X_SIZE = 5.0
     SET_BOX_Y_SIZE = 6.0
-    EXPECTED_COLUMN_SIZE = SET_BOX_X_SIZE + 1
-    EXPECTED_ROW_SIZE = SET_BOX_Y_SIZE + 1
+
+    # Our box is being created at (0,0), so it goes from (-2.5,-3) to (2.5,3)
+    # The default terrain grid step size is 1, and we expect our physics heightfield to be aligned to the grid,
+    # so the heightfield should go from (-2, -3) to (2, 3).
+    # The heightfield is also expected to create final vertex endpoints in each direction, so we expect the 
+    # X axis to create (-2, -1, 0, 1, 2) = 5 points, and the Y axis to create (-3, -2, -1, 0, 1, 2, 3) = 7 points
+    EXPECTED_COLUMN_SIZE = 5
+    EXPECTED_ROW_SIZE = 7
     
     # 1) Load the level
     hydra.open_base_level()
 
     # 2) Create test entity
-    test_entity = EditorEntity.create_editor_entity("TestEntity")
+    test_entity = EditorEntity.create_editor_entity_at(azmath.Vector3(0.0, 0.0, 0.0), "TestEntity")
     Report.result(Tests.create_test_entity, test_entity.id.IsValid())
 
     # 3) Start the Tracer to catch any errors and warnings

+ 14 - 16
AutomatedTesting/Levels/Terrain/TerrainPhysicsCollider_MaterialMapping_Works/TerrainPhysicsCollider_MaterialMapping_Works.prefab

@@ -386,14 +386,9 @@
                     "Parent Entity": "Entity_[1146574390643]",
                     "Transform Data": {
                         "Translate": [
-                            143.97714233398438,
-                            237.59323120117188,
-                            14.275827407836914
-                        ],
-                        "Rotate": [
-                            0.7218570113182068,
-                            -18.652305603027344,
-                            -0.16037322580814362
+                            143.75379943847656,
+                            237.58489990234375,
+                            14.5
                         ]
                     }
                 },
@@ -504,6 +499,11 @@
                     "$type": "EditorHeightfieldColliderComponent",
                     "Id": 13116465701951274134,
                     "ColliderConfiguration": {
+                        "Position": [
+                            0.4336090087890625,
+                            0.43792724609375,
+                            0.0
+                        ],
                         "MaterialSelection": {
                             "MaterialIds": [
                                 {},
@@ -516,6 +516,9 @@
                             ]
                         },
                         "propertyVisibilityFlags": 233
+                    },
+                    "DebugDrawSettings": {
+                        "LocallyEnabled": true
                     }
                 },
                 "Component_[13811745118082474953]": {
@@ -911,14 +914,9 @@
                     "Parent Entity": "Entity_[1146574390643]",
                     "Transform Data": {
                         "Translate": [
-                            143.9164581298828,
-                            224.15390014648438,
-                            14.144479751586914
-                        ],
-                        "Rotate": [
-                            3.441272258758545,
-                            -22.321136474609375,
-                            1.8760989904403687
+                            143.6857452392578,
+                            224.13734436035156,
+                            14.5
                         ]
                     }
                 },

+ 1 - 17
Code/Editor/GameEngine.cpp

@@ -477,7 +477,7 @@ void CGameEngine::SetLevelPath(const QString& path)
 
 bool CGameEngine::LoadLevel(
     [[maybe_unused]] bool bDeleteAIGraph,
-    bool bReleaseResources)
+    [[maybe_unused]] bool bReleaseResources)
 {
      m_bLevelLoaded = false;
     CLogFile::FormatLine("Loading map '%s' into engine...", m_levelPath.toUtf8().data());
@@ -502,22 +502,6 @@ bool CGameEngine::LoadLevel(
         }
     }
 
-    // Initialize physics grid.
-    if (bReleaseResources)
-    {
-        AZ::Aabb terrainAabb = AZ::Aabb::CreateFromPoint(AZ::Vector3::CreateZero());
-        AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(terrainAabb, &AzFramework::Terrain::TerrainDataRequests::GetTerrainAabb);
-        int physicsEntityGridSize = static_cast<int>(terrainAabb.GetXExtent());
-
-        //CryPhysics under performs if physicsEntityGridSize < nTerrainSize.
-        if (physicsEntityGridSize <= 0)
-        {
-            ICVar* pCvar = m_pISystem->GetIConsole()->GetCVar("e_PhysEntityGridSizeDefault");
-            physicsEntityGridSize = pCvar ? pCvar->GetIVal() : 4096;
-        }
-
-    }
-
     // Audio: notify audio of level loading start?
     GetIEditor()->GetObjectManager()->SendEvent(EVENT_REFRESH);
 

+ 16 - 4
Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.cpp

@@ -388,26 +388,38 @@ namespace Physics
         m_gridResolution = gridResolution;
     }
 
-    int32_t HeightfieldShapeConfiguration::GetNumColumns() const
+    int32_t HeightfieldShapeConfiguration::GetNumColumnVertices() const
     {
         return m_numColumns;
     }
 
-    void HeightfieldShapeConfiguration::SetNumColumns(int32_t numColumns)
+    void HeightfieldShapeConfiguration::SetNumColumnVertices(int32_t numColumns)
     {
         m_numColumns = numColumns;
     }
 
-    int32_t HeightfieldShapeConfiguration::GetNumRows() const
+    int32_t HeightfieldShapeConfiguration::GetNumRowVertices() const
     {
         return m_numRows;
     }
 
-    void HeightfieldShapeConfiguration::SetNumRows(int32_t numRows)
+    void HeightfieldShapeConfiguration::SetNumRowVertices(int32_t numRows)
     {
         m_numRows = numRows;
     }
 
+    int32_t HeightfieldShapeConfiguration::GetNumColumnSquares() const
+    {
+        // If we have N vertices, we have N - 1 squares ( ex: *--*--* is 3 vertices but 2 squares)
+        return AZStd::max(0, m_numColumns - 1);
+    }
+
+    int32_t HeightfieldShapeConfiguration::GetNumRowSquares() const
+    {
+        // If we have N vertices, we have N - 1 squares ( ex: *--*--* is 3 vertices but 2 squares)
+        return AZStd::max(0, m_numRows - 1);
+    }
+
     const AZStd::vector<Physics::HeightMaterialPoint>& HeightfieldShapeConfiguration::GetSamples() const
     {
         return m_samples;

+ 6 - 4
Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.h

@@ -233,10 +233,12 @@ namespace Physics
         void SetCachedNativeHeightfield(void* cachedNativeHeightfield);
         const AZ::Vector2& GetGridResolution() const;
         void SetGridResolution(const AZ::Vector2& gridSpacing);
-        int32_t GetNumColumns() const;
-        void SetNumColumns(int32_t numColumns);
-        int32_t GetNumRows() const;
-        void SetNumRows(int32_t numRows);
+        int32_t GetNumColumnVertices() const;
+        int32_t GetNumColumnSquares() const;
+        void SetNumColumnVertices(int32_t numColumns);
+        int32_t GetNumRowVertices() const;
+        int32_t GetNumRowSquares() const;
+        void SetNumRowVertices(int32_t numRows);
         const AZStd::vector<Physics::HeightMaterialPoint>& GetSamples() const;
         void ModifySample(int32_t row, int32_t column, const Physics::HeightMaterialPoint& point);
         void SetSamples(const AZStd::vector<Physics::HeightMaterialPoint>& samples);

+ 41 - 0
Code/Framework/AzFramework/AzFramework/Terrain/TerrainDataRequestBus.cpp

@@ -12,6 +12,47 @@
 
 namespace AzFramework::Terrain
 {
+    TerrainQueryRegion::TerrainQueryRegion(
+        const AZ::Vector3& startPoint, size_t numPointsX, size_t numPointsY, const AZ::Vector2& stepSize)
+        : m_startPoint(startPoint)
+        , m_numPointsX(numPointsX)
+        , m_numPointsY(numPointsY)
+        , m_stepSize(stepSize)
+    {
+    }
+
+    TerrainQueryRegion::TerrainQueryRegion(const AZ::Vector2& startPoint, size_t numPointsX, size_t numPointsY, const AZ::Vector2& stepSize)
+        : m_startPoint(startPoint)
+        , m_numPointsX(numPointsX)
+        , m_numPointsY(numPointsY)
+        , m_stepSize(stepSize)
+    {
+    }
+
+    TerrainQueryRegion TerrainQueryRegion::CreateFromAabbAndStepSize(const AZ::Aabb& region, const AZ::Vector2& stepSize)
+    {
+        TerrainQueryRegion queryRegion;
+
+        if (region.IsValid() && (stepSize.GetX() > 0.0f) && (stepSize.GetY() > 0.0f))
+        {
+            queryRegion.m_startPoint = region.GetMin();
+            queryRegion.m_stepSize = stepSize;
+
+            const AZ::Vector3 regionExtents = region.GetExtents();
+
+            // We'll treat the region as min-inclusive, max-exclusive.
+            // Ex: (1,1) - (6,6) with stepSize(1) will process 1, 2, 3, 4, 5 but not 6 in each direction.
+            // If the region is smaller than stepSize, make sure we still process at least the start point ("min-inclusive").
+            // However, when an extent is zero-size (i.e. min == max), we need to choose whether to follow the "min-inclusive" rule
+            // and include the start/end point, or the "max-exclusive" rule and exclude the start/end point. We're choosing to go
+            // with "max-exclusive", since it seems likely that a 0-length extent wasn't intended to include any points.
+            queryRegion.m_numPointsX = aznumeric_cast<size_t>(AZStd::ceil(regionExtents.GetX() / stepSize.GetX()));
+            queryRegion.m_numPointsY = aznumeric_cast<size_t>(AZStd::ceil(regionExtents.GetY() / stepSize.GetY()));
+        }
+
+        return queryRegion;
+    }
+
     // Create a handler that can be accessed from Python scripts to receive terrain change notifications.
     class TerrainDataNotificationHandler final
         : public AzFramework::Terrain::TerrainDataNotificationBus::Handler

+ 131 - 164
Code/Framework/AzFramework/AzFramework/Terrain/TerrainDataRequestBus.h

@@ -26,6 +26,108 @@ namespace AzFramework
         typedef AZStd::function<void(size_t xIndex, size_t yIndex, const SurfaceData::SurfacePoint& surfacePoint, bool terrainExists)> SurfacePointRegionFillCallback;
         typedef AZStd::function<void(const SurfaceData::SurfacePoint& surfacePoint, bool terrainExists)> SurfacePointListFillCallback;
 
+        //! Helper structure that defines a query region to use with the QueryRegion / QueryRegionAsync APIs.
+        struct TerrainQueryRegion
+        {
+            AZ_TYPE_INFO(TerrainQueryRegion, "{DE3F634D-9689-4FBC-9F43-A39CFDF425D0}");
+
+            TerrainQueryRegion() = default;
+            ~TerrainQueryRegion() = default;
+
+            TerrainQueryRegion(const AZ::Vector3& startPoint, size_t numPointsX, size_t numPointsY, const AZ::Vector2& stepSize);
+            TerrainQueryRegion(const AZ::Vector2& startPoint, size_t numPointsX, size_t numPointsY, const AZ::Vector2& stepSize);
+
+            static TerrainQueryRegion CreateFromAabbAndStepSize(const AZ::Aabb& region, const AZ::Vector2& stepSize);
+
+            AZ::Vector3 m_startPoint{ 0.0f }; //! The starting point for the region query
+            AZ::Vector2 m_stepSize{ 0.0f }; //! The step size to use for advancing to the next point to query
+            size_t m_numPointsX{ 0 }; //! The total number of points to query in the X direction
+            size_t m_numPointsY{ 0 }; //! The total number of points to query in the Y direction
+        };
+
+        //! A JobContext used to run jobs spawned by calls to the various Query*Async functions.
+        class TerrainJobContext : public AZ::JobContext
+        {
+        public:
+            TerrainJobContext(AZ::JobManager& jobManager, int numJobsToComplete)
+                : JobContext(jobManager)
+                , m_numJobsToComplete(numJobsToComplete)
+            {
+                AZ_Assert(numJobsToComplete > 0, "Invalid number of jobs: %d", numJobsToComplete);
+            }
+
+            // When a terrain job context is cancelled, all associated
+            // jobs are still guaranteed to at least begin processing,
+            // and if any QueryAsyncParams::m_completionCallback was
+            // set it's guaranteed to be called even in the event of a
+            // cancellation. If a job only begins processing after its
+            // associated job context has been cancelled, no processing
+            // will occur and the callback will be invoked immediately,
+            // otherwise the job may either run to completion or cease
+            // processing early; the callback is invoked in all cases,
+            // provided one was specified with the original request.
+            void Cancel()
+            {
+                m_isCancelled = true;
+            }
+
+            // Was this TerrainJobContext cancelled?
+            bool IsCancelled() const
+            {
+                return m_isCancelled;
+            }
+
+            // Called by the TerrainSystem when a job associated with
+            // this TerrainJobContext completes. Returns true if this
+            // was the final job to be completed, or false otherwise.
+            bool OnJobCompleted()
+            {
+                return (--m_numJobsToComplete <= 0);
+            }
+
+        private:
+            AZStd::atomic_int m_numJobsToComplete = 0;
+            AZStd::atomic_bool m_isCancelled = false;
+        };
+
+        //! Alias for an optional callback function to invoke when the various Query*Async functions complete.
+        //! The TerrainJobContext, returned from the original Query*Async function call, is passed as a param
+        //! to the callback function so it can be queried to see if the job was cancelled or completed normally.
+        typedef AZStd::function<void(AZStd::shared_ptr<TerrainJobContext>)> QueryAsyncCompleteCallback;
+
+        //! A parameter group struct that can optionally be passed to the various Query*Async API functions.
+        struct QueryAsyncParams
+        {
+            //! This constant can be used with m_desiredNumberOfJobs to request the maximum
+            //! number of jobs possible for splitting up async terrain requests based on the
+            //! maximum number of cores / job threads on the current PC. It's a symbolic number,
+            //! not a literal one, since the maximum number of available jobs will change from
+            //! machine to machine.
+            static constexpr int32_t NumJobsMax = -1;
+
+            //! This constant is used with m_desiredNumberOfJobs to set the default number of jobs
+            //! for splitting up async terrain requests. By default, we use a single job so that the
+            //! request runs asynchronously but only consumes a single thread.
+            static constexpr int32_t NumJobsDefault = 1;
+
+            //! The desired maximum number of jobs to split async terrain requests into.
+            //! The actual number of jobs used will be clamped based on the number of available job manager threads,
+            //! the desired number of jobs, and the minimum number of positions that should be processed per job.
+            int32_t m_desiredNumberOfJobs = NumJobsDefault;
+
+            //! This constant is used with m_minPositionsPerJob to set the default minimum number
+            //! of positions to process per async terrain request job. The higher the number, the more it will
+            //! limit the maximum number of simultaneous jobs per async terrain request.
+            static constexpr int32_t MinPositionsPerJobDefault = 8;
+
+            //! The minimum number of positions per async terrain request job.
+            int32_t m_minPositionsPerJob = MinPositionsPerJobDefault;
+
+            //! The callback function that will be invoked when a call to a Query*Async function completes.
+            //! If the job is cancelled, the completion callback will not be invoked.
+            QueryAsyncCompleteCallback m_completionCallback = nullptr;
+        };
+
         //! Shared interface for terrain system implementations
         class TerrainDataRequests
             : public AZ::EBusSharedDispatchTraits<TerrainDataRequests>
@@ -140,54 +242,32 @@ namespace AzFramework
                 Sampler sampleFilter = Sampler::DEFAULT,
                 bool* terrainExistsPtr = nullptr) const = 0;
 
+            //! Flags for selecting the combination of data to query when querying a list or region.
+            //! The flags determine which subset of data in the SurfacePoint struct will be valid in the FillCallback.
+            enum TerrainDataMask : uint8_t
+            {
+                Heights     = 0b00000001,   //! Query height data
+                Normals     = 0b00000010,   //! Query normal data
+                SurfaceData = 0b00000100,   //! Query surface types and weights
+                All         = 0b11111111    //! Query for every available data channel (heights, normals, surface data)
+            };
+
             //! Given a list of XY coordinates, call the provided callback function with surface data corresponding to each
             //! XY coordinate in the list.
-            virtual void ProcessHeightsFromList(const AZStd::span<const AZ::Vector3>& inPositions,
-                SurfacePointListFillCallback perPositionCallback,
-                Sampler sampleFilter = Sampler::DEFAULT) const = 0;
-            virtual void ProcessNormalsFromList(const AZStd::span<const AZ::Vector3>& inPositions,
-                SurfacePointListFillCallback perPositionCallback,
-                Sampler sampleFilter = Sampler::DEFAULT) const = 0;
-            virtual void ProcessSurfaceWeightsFromList(const AZStd::span<const AZ::Vector3>& inPositions,
-                SurfacePointListFillCallback perPositionCallback,
-                Sampler sampleFilter = Sampler::DEFAULT) const = 0;
-            virtual void ProcessSurfacePointsFromList(const AZStd::span<const AZ::Vector3>& inPositions,
+            virtual void QueryList(const AZStd::span<const AZ::Vector3>& inPositions,
+                TerrainDataMask requestedData,
                 SurfacePointListFillCallback perPositionCallback,
                 Sampler sampleFilter = Sampler::DEFAULT) const = 0;
-            virtual void ProcessHeightsFromListOfVector2(const AZStd::span<const AZ::Vector2>& inPositions,
-                SurfacePointListFillCallback perPositionCallback,
-                Sampler sampleFilter = Sampler::DEFAULT) const = 0;
-            virtual void ProcessNormalsFromListOfVector2(const AZStd::span<const AZ::Vector2>& inPositions,
-                SurfacePointListFillCallback perPositionCallback,
-                Sampler sampleFilter = Sampler::DEFAULT) const = 0;
-            virtual void ProcessSurfaceWeightsFromListOfVector2(const AZStd::span<const AZ::Vector2>& inPositions,
-                SurfacePointListFillCallback perPositionCallback,
-                Sampler sampleFilter = Sampler::DEFAULT) const = 0;
-            virtual void ProcessSurfacePointsFromListOfVector2(const AZStd::span<const AZ::Vector2>& inPositions,
+            virtual void QueryListOfVector2(const AZStd::span<const AZ::Vector2>& inPositions,
+                TerrainDataMask requestedData,
                 SurfacePointListFillCallback perPositionCallback,
                 Sampler sampleFilter = Sampler::DEFAULT) const = 0;
 
-            //! Returns the number of samples for a given region and step size. The first and second
-            //! elements of the pair correspond to the X and Y sample counts respectively.
-            virtual AZStd::pair<size_t, size_t> GetNumSamplesFromRegion(const AZ::Aabb& inRegion,
-                const AZ::Vector2& stepSize, Sampler samplerType) const = 0;
-
-            //! Given a region(aabb) and a step size, call the provided callback function with surface data corresponding to the
+            //! Given a terrain query region, call the provided callback function with terrain data corresponding to the
             //! coordinates in the region.
-            virtual void ProcessHeightsFromRegion(const AZ::Aabb& inRegion,
-                const AZ::Vector2& stepSize,
-                SurfacePointRegionFillCallback perPositionCallback,
-                Sampler sampleFilter = Sampler::DEFAULT) const = 0;
-            virtual void ProcessNormalsFromRegion(const AZ::Aabb& inRegion,
-                const AZ::Vector2& stepSize,
-                SurfacePointRegionFillCallback perPositionCallback,
-                Sampler sampleFilter = Sampler::DEFAULT) const = 0;
-            virtual void ProcessSurfaceWeightsFromRegion(const AZ::Aabb& inRegion,
-                const AZ::Vector2& stepSize,
-                SurfacePointRegionFillCallback perPositionCallback,
-                Sampler sampleFilter = Sampler::DEFAULT) const = 0;
-            virtual void ProcessSurfacePointsFromRegion(const AZ::Aabb& inRegion,
-                const AZ::Vector2& stepSize,
+            virtual void QueryRegion(
+                const TerrainQueryRegion& queryRegion,
+                TerrainDataMask requestedData,
                 SurfacePointRegionFillCallback perPositionCallback,
                 Sampler sampleFilter = Sampler::DEFAULT) const = 0;
 
@@ -197,139 +277,26 @@ namespace AzFramework
             //! Given a ray, return the closest intersection with terrain.
             virtual RenderGeometry::RayResult GetClosestIntersection(const RenderGeometry::RayRequest& ray) const = 0;
 
-            //! A JobContext used to run jobs spawned by calls to the various Process*Async functions.
-            class TerrainJobContext : public AZ::JobContext
-            {
-            public:
-                TerrainJobContext(AZ::JobManager& jobManager,
-                                  int numJobsToComplete)
-                    : JobContext(jobManager)
-                    , m_numJobsToComplete(numJobsToComplete)
-                {
-                }
-
-                // When a terrain job context is cancelled, all associated
-                // jobs are still guaranteed to at least begin processing,
-                // and if any ProcessAsyncParams::m_completionCallback was
-                // set it's guaranteed to be called even in the event of a
-                // cancellation. If a job only begins processing after its
-                // associated job context has been cancelled, no processing
-                // will occur and the callback will be invoked immediately,
-                // otherwise the job may either run to completion or cease
-                // processing early; the callback is invoked in all cases,
-                // provided one was specified with the original request.
-                void Cancel() { m_isCancelled = true; }
-
-                // Was this TerrainJobContext cancelled?
-                bool IsCancelled() const { return m_isCancelled; }
-
-                // Called by the TerrainSystem when a job associated with
-                // this TerrainJobContext completes. Returns true if this
-                // was the final job to be completed, or false otherwise.
-                bool OnJobCompleted() { return (--m_numJobsToComplete == 0); }
-
-            private:
-                AZStd::atomic_int m_numJobsToComplete = 0;
-                AZStd::atomic_bool m_isCancelled = false;
-            };
-
-            //! Alias for an optional callback function to invoke when the various Process*Async functions complete.
-            //! The TerrainJobContext, returned from the original Process*Async function call, is passed as a param
-            //! to the callback function so it can be queried to see if the job was cancelled or completed normally.
-            typedef AZStd::function<void(AZStd::shared_ptr<TerrainJobContext>)> ProcessAsyncCompleteCallback;
-
-            //! A parameter group struct that can optionally be passed to the various Process*Async API functions.
-            struct ProcessAsyncParams
-            {
-                //! The default minimum number of positions per async terrain request job.
-                static constexpr int32_t MinPositionsPerJobDefault = 8;
-
-                //! The maximum number of jobs which async terrain requests will be split into.
-                //! This is not the value itself, rather a constant that can be used to request
-                //! the work be split into the maximum number of job manager threads available.
-                static constexpr int32_t NumJobsMax = -1;
-
-                //! The default number of jobs which async terrain requests will be split into.
-                static constexpr int32_t NumJobsDefault = 1;
-
-                //! The desired number of jobs to split async terrain requests into.
-                //! The actual value used will be clamped to the number of available job manager threads.
-                int32_t m_desiredNumberOfJobs = NumJobsDefault;
-
-                //! The minimum number of positions per async terrain request job.
-                int32_t m_minPositionsPerJob = MinPositionsPerJobDefault;
-
-                //! The callback function that will be invoked when a call to a Process*Async function completes.
-                //! If the job is cancelled, the completion callback will not be invoked.
-                ProcessAsyncCompleteCallback m_completionCallback = nullptr;
-            };
-
-            //! Asynchronous versions of the various 'Process*' API functions declared above.
+            //! Asynchronous versions of the various 'Query*' API functions declared above.
             //! It's the responsibility of the caller to ensure all callbacks are threadsafe.
-            virtual AZStd::shared_ptr<TerrainJobContext> ProcessHeightsFromListAsync(
-                const AZStd::span<const AZ::Vector3>& inPositions,
-                SurfacePointListFillCallback perPositionCallback,
-                Sampler sampleFilter = Sampler::DEFAULT,
-                AZStd::shared_ptr<ProcessAsyncParams> params = nullptr) const = 0;
-            virtual AZStd::shared_ptr<TerrainJobContext> ProcessNormalsFromListAsync(
-                const AZStd::span<const AZ::Vector3>& inPositions,
-                SurfacePointListFillCallback perPositionCallback,
-                Sampler sampleFilter = Sampler::DEFAULT,
-                AZStd::shared_ptr<ProcessAsyncParams> params = nullptr) const = 0;
-            virtual AZStd::shared_ptr<TerrainJobContext> ProcessSurfaceWeightsFromListAsync(
+            virtual AZStd::shared_ptr<TerrainJobContext> QueryListAsync(
                 const AZStd::span<const AZ::Vector3>& inPositions,
+                TerrainDataMask requestedData,
                 SurfacePointListFillCallback perPositionCallback,
                 Sampler sampleFilter = Sampler::DEFAULT,
-                AZStd::shared_ptr<ProcessAsyncParams> params = nullptr) const = 0;
-            virtual AZStd::shared_ptr<TerrainJobContext> ProcessSurfacePointsFromListAsync(
-                const AZStd::span<const AZ::Vector3>& inPositions,
-                SurfacePointListFillCallback perPositionCallback,
-                Sampler sampleFilter = Sampler::DEFAULT,
-                AZStd::shared_ptr<ProcessAsyncParams> params = nullptr) const = 0;
-            virtual AZStd::shared_ptr<TerrainJobContext> ProcessHeightsFromListOfVector2Async(
-                const AZStd::span<const AZ::Vector2>& inPositions,
-                SurfacePointListFillCallback perPositionCallback,
-                Sampler sampleFilter = Sampler::DEFAULT,
-                AZStd::shared_ptr<ProcessAsyncParams> params = nullptr) const = 0;
-            virtual AZStd::shared_ptr<TerrainJobContext> ProcessNormalsFromListOfVector2Async(
-                const AZStd::span<const AZ::Vector2>& inPositions,
-                SurfacePointListFillCallback perPositionCallback,
-                Sampler sampleFilter = Sampler::DEFAULT,
-                AZStd::shared_ptr<ProcessAsyncParams> params = nullptr) const = 0;
-            virtual AZStd::shared_ptr<TerrainJobContext> ProcessSurfaceWeightsFromListOfVector2Async(
-                const AZStd::span<const AZ::Vector2>& inPositions,
-                SurfacePointListFillCallback perPositionCallback,
-                Sampler sampleFilter = Sampler::DEFAULT,
-                AZStd::shared_ptr<ProcessAsyncParams> params = nullptr) const = 0;
-            virtual AZStd::shared_ptr<TerrainJobContext> ProcessSurfacePointsFromListOfVector2Async(
+                AZStd::shared_ptr<QueryAsyncParams> params = nullptr) const = 0;
+            virtual AZStd::shared_ptr<TerrainJobContext> QueryListOfVector2Async(
                 const AZStd::span<const AZ::Vector2>& inPositions,
+                TerrainDataMask requestedData,
                 SurfacePointListFillCallback perPositionCallback,
                 Sampler sampleFilter = Sampler::DEFAULT,
-                AZStd::shared_ptr<ProcessAsyncParams> params = nullptr) const = 0;
-            virtual AZStd::shared_ptr<TerrainJobContext> ProcessHeightsFromRegionAsync(
-                const AZ::Aabb& inRegion,
-                const AZ::Vector2& stepSize,
-                SurfacePointRegionFillCallback perPositionCallback,
-                Sampler sampleFilter = Sampler::DEFAULT,
-                AZStd::shared_ptr<ProcessAsyncParams> params = nullptr) const = 0;
-            virtual AZStd::shared_ptr<TerrainJobContext> ProcessNormalsFromRegionAsync(
-                const AZ::Aabb& inRegion,
-                const AZ::Vector2& stepSize,
-                SurfacePointRegionFillCallback perPositionCallback,
-                Sampler sampleFilter = Sampler::DEFAULT,
-                AZStd::shared_ptr<ProcessAsyncParams> params = nullptr) const = 0;
-            virtual AZStd::shared_ptr<TerrainJobContext> ProcessSurfaceWeightsFromRegionAsync(
-                const AZ::Aabb& inRegion,
-                const AZ::Vector2& stepSize,
-                SurfacePointRegionFillCallback perPositionCallback,
-                Sampler sampleFilter = Sampler::DEFAULT,
-                AZStd::shared_ptr<ProcessAsyncParams> params = nullptr) const = 0;
-            virtual AZStd::shared_ptr<TerrainJobContext> ProcessSurfacePointsFromRegionAsync(
-                const AZ::Aabb& inRegion,
-                const AZ::Vector2& stepSize,
+                AZStd::shared_ptr<QueryAsyncParams> params = nullptr) const = 0;
+            virtual AZStd::shared_ptr<TerrainJobContext> QueryRegionAsync(
+                const TerrainQueryRegion& queryRegion,
+                TerrainDataMask requestedData,
                 SurfacePointRegionFillCallback perPositionCallback,
                 Sampler sampleFilter = Sampler::DEFAULT,
-                AZStd::shared_ptr<ProcessAsyncParams> params = nullptr) const = 0;
+                AZStd::shared_ptr<QueryAsyncParams> params = nullptr) const = 0;
         };
         using TerrainDataRequestBus = AZ::EBus<TerrainDataRequests>;
 

+ 31 - 45
Code/Framework/AzFramework/Tests/Mocks/Terrain/MockTerrainDataRequestBus.h

@@ -78,60 +78,46 @@ namespace UnitTest
             GetSurfacePointFromVector2, void(const AZ::Vector2&, AzFramework::SurfaceData::SurfacePoint&, Sampler, bool*));
         MOCK_CONST_METHOD5(
             GetSurfacePointFromFloats, void(float, float, AzFramework::SurfaceData::SurfacePoint&, Sampler, bool*));
-        MOCK_CONST_METHOD3(
-            ProcessHeightsFromList, void(const AZStd::span<const AZ::Vector3>&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler));
-        MOCK_CONST_METHOD3(
-            ProcessNormalsFromList, void(const AZStd::span<const AZ::Vector3>&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler));
-        MOCK_CONST_METHOD3(
-            ProcessSurfaceWeightsFromList, void(const AZStd::span<const AZ::Vector3>&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler));
-        MOCK_CONST_METHOD3(
-            ProcessSurfacePointsFromList, void(const AZStd::span<const AZ::Vector3>&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler));
-        MOCK_CONST_METHOD3(
-            ProcessHeightsFromListOfVector2, void(const AZStd::span<const AZ::Vector2>&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler));
-        MOCK_CONST_METHOD3(
-            ProcessNormalsFromListOfVector2, void(const AZStd::span<const AZ::Vector2>&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler));
-        MOCK_CONST_METHOD3(
-            ProcessSurfaceWeightsFromListOfVector2, void(const AZStd::span<const AZ::Vector2>&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler));
-        MOCK_CONST_METHOD3(
-            ProcessSurfacePointsFromListOfVector2, void(const AZStd::span<const AZ::Vector2>&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler));
-        MOCK_CONST_METHOD3(
-            GetNumSamplesFromRegion, AZStd::pair<size_t, size_t>(const AZ::Aabb&, const AZ::Vector2&, Sampler));
-        MOCK_CONST_METHOD4(
-            ProcessHeightsFromRegion, void(const AZ::Aabb&, const AZ::Vector2&, AzFramework::Terrain::SurfacePointRegionFillCallback, Sampler));
         MOCK_CONST_METHOD4(
-            ProcessNormalsFromRegion, void(const AZ::Aabb&, const AZ::Vector2&, AzFramework::Terrain::SurfacePointRegionFillCallback, Sampler));
+            QueryList, void(const AZStd::span<const AZ::Vector3>&, TerrainDataMask, AzFramework::Terrain::SurfacePointListFillCallback, Sampler));
         MOCK_CONST_METHOD4(
-            ProcessSurfaceWeightsFromRegion, void(const AZ::Aabb&, const AZ::Vector2&, AzFramework::Terrain::SurfacePointRegionFillCallback, Sampler));
+            QueryListOfVector2, void(const AZStd::span<const AZ::Vector2>&, TerrainDataMask, AzFramework::Terrain::SurfacePointListFillCallback, Sampler));
+        MOCK_CONST_METHOD3(
+            GetNumSamplesFromRegion, AZStd::pair<size_t, size_t>(const AZ::Aabb&, const AZ::Vector2&, Sampler));
         MOCK_CONST_METHOD4(
-            ProcessSurfacePointsFromRegion, void(const AZ::Aabb&, const AZ::Vector2&, AzFramework::Terrain::SurfacePointRegionFillCallback, Sampler));
+            QueryRegion,
+            void(
+                const AzFramework::Terrain::TerrainQueryRegion&,
+                TerrainDataMask,
+                AzFramework::Terrain::SurfacePointRegionFillCallback,
+                Sampler));
         MOCK_CONST_METHOD0(
             GetTerrainRaycastEntityContextId, AzFramework::EntityContextId());
         MOCK_CONST_METHOD1(
             GetClosestIntersection, AzFramework::RenderGeometry::RayResult(const AzFramework::RenderGeometry::RayRequest&));
-        MOCK_CONST_METHOD4(
-            ProcessHeightsFromListAsync, AZStd::shared_ptr<TerrainJobContext>(const AZStd::span<const AZ::Vector3>&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler, AZStd::shared_ptr<ProcessAsyncParams>));
-        MOCK_CONST_METHOD4(
-            ProcessNormalsFromListAsync, AZStd::shared_ptr<TerrainJobContext>(const AZStd::span<const AZ::Vector3>&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler, AZStd::shared_ptr<ProcessAsyncParams>));
-        MOCK_CONST_METHOD4(
-            ProcessSurfaceWeightsFromListAsync, AZStd::shared_ptr<TerrainJobContext>(const AZStd::span<const AZ::Vector3>&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler, AZStd::shared_ptr<ProcessAsyncParams>));
-        MOCK_CONST_METHOD4(
-            ProcessSurfacePointsFromListAsync, AZStd::shared_ptr<TerrainJobContext>(const AZStd::span<const AZ::Vector3>&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler, AZStd::shared_ptr<ProcessAsyncParams>));
-        MOCK_CONST_METHOD4(
-            ProcessHeightsFromListOfVector2Async, AZStd::shared_ptr<TerrainJobContext>(const AZStd::span<const AZ::Vector2>&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler, AZStd::shared_ptr<ProcessAsyncParams>));
-        MOCK_CONST_METHOD4(
-            ProcessNormalsFromListOfVector2Async, AZStd::shared_ptr<TerrainJobContext>(const AZStd::span<const AZ::Vector2>&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler, AZStd::shared_ptr<ProcessAsyncParams>));
-        MOCK_CONST_METHOD4(
-            ProcessSurfaceWeightsFromListOfVector2Async, AZStd::shared_ptr<TerrainJobContext>(const AZStd::span<const AZ::Vector2>&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler, AZStd::shared_ptr<ProcessAsyncParams>));
-        MOCK_CONST_METHOD4(
-            ProcessSurfacePointsFromListOfVector2Async, AZStd::shared_ptr<TerrainJobContext>(const AZStd::span<const AZ::Vector2>&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler, AZStd::shared_ptr<ProcessAsyncParams>));
-        MOCK_CONST_METHOD5(
-            ProcessHeightsFromRegionAsync, AZStd::shared_ptr<TerrainJobContext>(const AZ::Aabb&, const AZ::Vector2&, AzFramework::Terrain::SurfacePointRegionFillCallback, Sampler, AZStd::shared_ptr<ProcessAsyncParams>));
         MOCK_CONST_METHOD5(
-            ProcessNormalsFromRegionAsync, AZStd::shared_ptr<TerrainJobContext>(const AZ::Aabb&, const AZ::Vector2&, AzFramework::Terrain::SurfacePointRegionFillCallback, Sampler, AZStd::shared_ptr<ProcessAsyncParams>));
+            QueryListAsync,
+            AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext>(
+                const AZStd::span<const AZ::Vector3>&,
+                TerrainDataMask,
+                AzFramework::Terrain::SurfacePointListFillCallback,
+                Sampler,
+                AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams>));
         MOCK_CONST_METHOD5(
-            ProcessSurfaceWeightsFromRegionAsync, AZStd::shared_ptr<TerrainJobContext>(const AZ::Aabb&, const AZ::Vector2&, AzFramework::Terrain::SurfacePointRegionFillCallback, Sampler, AZStd::shared_ptr<ProcessAsyncParams>));
+            QueryListOfVector2Async,
+            AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext>(
+                const AZStd::span<const AZ::Vector2>&,
+                TerrainDataMask,
+                AzFramework::Terrain::SurfacePointListFillCallback,
+                Sampler,
+                AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams>));
         MOCK_CONST_METHOD5(
-            ProcessSurfacePointsFromRegionAsync, AZStd::shared_ptr<TerrainJobContext>(const AZ::Aabb&, const AZ::Vector2&, AzFramework::Terrain::SurfacePointRegionFillCallback, Sampler, AZStd::shared_ptr<ProcessAsyncParams>));
-        
+            QueryRegionAsync,
+            AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext>(
+                const AzFramework::Terrain::TerrainQueryRegion&,
+                TerrainDataMask,
+                AzFramework::Terrain::SurfacePointRegionFillCallback,
+                Sampler,
+                AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams>));
     };
 } // namespace UnitTest

+ 22 - 9
Gems/PhysX/Code/Source/EditorHeightfieldColliderComponent.cpp

@@ -33,7 +33,7 @@ namespace PhysX
 {
     AZ_CVAR(float, physx_heightfieldDebugDrawDistance, 50.0f, nullptr,
         AZ::ConsoleFunctorFlags::Null, "Distance for PhysX Heightfields debug visualization.");
-    AZ_CVAR(bool, physx_heightfieldDebugDrawBoundingBox, true,
+    AZ_CVAR(bool, physx_heightfieldDebugDrawBoundingBox, false,
         nullptr, AZ::ConsoleFunctorFlags::Null, "Draw the bounding box used for heightfield debug visualization.");
 
     void EditorHeightfieldColliderComponent::Reflect(AZ::ReflectContext* context)
@@ -168,8 +168,14 @@ namespace PhysX
     void EditorHeightfieldColliderComponent::BuildGameEntity(AZ::Entity* gameEntity)
     {
         auto* heightfieldColliderComponent = gameEntity->CreateComponent<HeightfieldColliderComponent>();
+
+        // Create an empty shapeConfig for initializing the component's ShapeConfiguration.
+        // The actual shapeConfig for the component will get filled out at runtime as everything initializes,
+        // so the values set here during initialization don't matter.
+        AZStd::shared_ptr<Physics::HeightfieldShapeConfiguration> shapeConfig{ aznew Physics::HeightfieldShapeConfiguration() };
+
         heightfieldColliderComponent->SetShapeConfiguration(
-            { AZStd::make_shared<Physics::ColliderConfiguration>(m_colliderConfig), m_shapeConfig });
+            { AZStd::make_shared<Physics::ColliderConfiguration>(m_colliderConfig), shapeConfig });
     }
 
     void EditorHeightfieldColliderComponent::OnHeightfieldDataChanged(const AZ::Aabb& dirtyRegion, 
@@ -196,6 +202,13 @@ namespace PhysX
 
     void EditorHeightfieldColliderComponent::InitStaticRigidBody()
     {
+        const AZ::Transform baseTransform = GetWorldTM();
+        AzPhysics::StaticRigidBodyConfiguration configuration;
+        configuration.m_orientation = baseTransform.GetRotation();
+        configuration.m_position = baseTransform.GetTranslation();
+        configuration.m_entityId = GetEntityId();
+        configuration.m_debugName = GetEntity()->GetName();
+
         // Get the transform from the HeightfieldProvider.  Because rotation and scale can indirectly affect how the heightfield itself
         // is computed and the size of the heightfield, it's possible that the HeightfieldProvider will provide a different transform
         // back to us than the one that's directly on that entity.
@@ -203,11 +216,11 @@ namespace PhysX
         Physics::HeightfieldProviderRequestsBus::EventResult(
             transform, GetEntityId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldTransform);
 
-        AzPhysics::StaticRigidBodyConfiguration configuration;
-        configuration.m_orientation = transform.GetRotation();
-        configuration.m_position = transform.GetTranslation();
-        configuration.m_entityId = GetEntityId();
-        configuration.m_debugName = GetEntity()->GetName();
+        // Because the heightfield's transform may not match the entity's transform, use the heightfield transform
+        // to generate an offset rotation/position from the entity's transform for the collider configuration.
+        m_colliderConfig.m_rotation = transform.GetRotation() * baseTransform.GetRotation().GetInverseFull();
+        m_colliderConfig.m_position =
+            m_colliderConfig.m_rotation.TransformVector(transform.GetTranslation() - baseTransform.GetTranslation());
 
         // Update material selection from the mapping
         Utils::SetMaterialsFromHeightfieldProvider(GetEntityId(), m_colliderConfig.m_materialSelection);
@@ -286,8 +299,8 @@ namespace PhysX
         if (!shouldRecreateHeightfield)
         {
             Physics::HeightfieldShapeConfiguration baseConfiguration = Utils::CreateBaseHeightfieldShapeConfiguration(GetEntityId());
-            shouldRecreateHeightfield = shouldRecreateHeightfield || (baseConfiguration.GetNumRows() != m_shapeConfig->GetNumRows());
-            shouldRecreateHeightfield = shouldRecreateHeightfield || (baseConfiguration.GetNumColumns() != m_shapeConfig->GetNumColumns());
+            shouldRecreateHeightfield = shouldRecreateHeightfield || (baseConfiguration.GetNumRowVertices() != m_shapeConfig->GetNumRowVertices());
+            shouldRecreateHeightfield = shouldRecreateHeightfield || (baseConfiguration.GetNumColumnVertices() != m_shapeConfig->GetNumColumnVertices());
             shouldRecreateHeightfield = shouldRecreateHeightfield || (baseConfiguration.GetMinHeightBounds() != m_shapeConfig->GetMinHeightBounds());
             shouldRecreateHeightfield = shouldRecreateHeightfield || (baseConfiguration.GetMaxHeightBounds() != m_shapeConfig->GetMaxHeightBounds());
         }

+ 2 - 5
Gems/PhysX/Code/Source/HeightfieldColliderComponent.cpp

@@ -7,6 +7,7 @@
  */
 
 #include <AzCore/Component/Entity.h>
+#include <AzCore/Component/TransformBus.h>
 #include <AzCore/Serialization/SerializeContext.h>
 #include <AzCore/std/smart_ptr/make_shared.h>
 #include <AzFramework/Physics/Collision/CollisionGroups.h>
@@ -110,12 +111,8 @@ namespace PhysX
 
     void HeightfieldColliderComponent::InitStaticRigidBody()
     {
-        // Get the transform from the HeightfieldProvider.  Because rotation and scale can indirectly affect how the heightfield itself
-        // is computed and the size of the heightfield, it's possible that the HeightfieldProvider will provide a different transform
-        // back to us than the one that's directly on that entity.
         AZ::Transform transform = AZ::Transform::CreateIdentity();
-        Physics::HeightfieldProviderRequestsBus::EventResult(
-            transform, GetEntityId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldTransform);
+        AZ::TransformBus::EventResult(transform, GetEntityId(), &AZ::TransformInterface::GetWorldTM);
 
         AzPhysics::StaticRigidBodyConfiguration configuration;
         configuration.m_orientation = transform.GetRotation();

+ 7 - 7
Gems/PhysX/Code/Source/Utils.cpp

@@ -136,8 +136,8 @@ namespace PhysX
 
             const AZ::Vector2& gridSpacing = heightfieldConfig.GetGridResolution();
 
-            const int32_t numCols = heightfieldConfig.GetNumColumns();
-            const int32_t numRows = heightfieldConfig.GetNumRows();
+            const int32_t numCols = heightfieldConfig.GetNumColumnVertices();
+            const int32_t numRows = heightfieldConfig.GetNumRowVertices();
 
             const float rowScale = gridSpacing.GetX();
             const float colScale = gridSpacing.GetY();
@@ -397,11 +397,11 @@ namespace PhysX
                             static_cast<const Physics::HeightfieldShapeConfiguration&>(shapeConfiguration);
 
                         // PhysX heightfields have the origin at the corner, not the center, so add an offset to the passed-in transform
-                        // to account for this difference.
+                        // to account for this difference. 
                         const AZ::Vector2 gridSpacing = heightfieldConfig.GetGridResolution();
                         AZ::Vector3 offset(
-                            -(gridSpacing.GetX() * heightfieldConfig.GetNumColumns() / 2.0f),
-                            -(gridSpacing.GetY() * heightfieldConfig.GetNumRows() / 2.0f),
+                            -(gridSpacing.GetX() * heightfieldConfig.GetNumColumnSquares() / 2.0f),
+                            -(gridSpacing.GetY() * heightfieldConfig.GetNumRowSquares() / 2.0f),
                             0.0f);
 
                         // PhysX heightfields are always defined to have the height in the Y direction, not the Z direction, so we need
@@ -1581,8 +1581,8 @@ namespace PhysX
             Physics::HeightfieldProviderRequestsBus::Event(
                 entityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, numColumns, numRows);
 
-            configuration.SetNumRows(numRows);
-            configuration.SetNumColumns(numColumns);
+            configuration.SetNumRowVertices(numRows);
+            configuration.SetNumColumnVertices(numColumns);
 
             float minHeightBounds = 0.0f;
             float maxHeightBounds = 0.0f;

+ 28 - 11
Gems/PhysX/Code/Tests/EditorHeightfieldColliderComponentTests.cpp

@@ -114,11 +114,18 @@ namespace PhysXEditorTests
             SetupMockMethods(*m_editorMockShapeRequests.get());
             m_editorEntity->Activate();
 
+            // Notify the Editor entity that the heightfield data changed so that it refreshes itself before we build
+            // the corresponding game entity.
+            Physics::HeightfieldProviderNotificationBus::Broadcast(
+                &Physics::HeightfieldProviderNotificationBus::Events::OnHeightfieldDataChanged, AZ::Aabb::CreateNull(),
+                Physics::HeightfieldProviderNotifications::HeightfieldChangeMask::CreateEnd);
+
             m_gameEntity = TestCreateActiveGameEntityFromEditorEntity(m_editorEntity.get());
             m_gameMockShapeRequests = AZStd::make_unique<NiceMock<UnitTest::MockPhysXHeightfieldProvider>>(m_gameEntity->GetId());
             SetupMockMethods(*m_gameMockShapeRequests.get());
             m_gameEntity->Activate();
 
+            // Send the notification a second time so that the game entity gets refreshed as well.
             Physics::HeightfieldProviderNotificationBus::Broadcast(
                 &Physics::HeightfieldProviderNotificationBus::Events::OnHeightfieldDataChanged, AZ::Aabb::CreateNull(),
                 Physics::HeightfieldProviderNotifications::HeightfieldChangeMask::CreateEnd);
@@ -282,19 +289,18 @@ namespace PhysXEditorTests
         Physics::HeightfieldProviderRequestsBus::EventResult(
             samples, gameEntityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightsAndMaterials);
 
+        float minHeightBounds{ 0.0f };
+        float maxHeightBounds{ 0.0f };
+        Physics::HeightfieldProviderRequestsBus::Event(
+            gameEntityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldHeightBounds, minHeightBounds, maxHeightBounds);
+
+        const float halfBounds{ (maxHeightBounds - minHeightBounds) / 2.0f };
+        const float scaleFactor = (maxHeightBounds <= minHeightBounds) ? 1.0f : AZStd::numeric_limits<int16_t>::max() / halfBounds;
+
         for (int sampleRow = 0; sampleRow < numRows; ++sampleRow)
         {
             for (int sampleColumn = 0; sampleColumn < numColumns; ++sampleColumn)
             {
-                float minHeightBounds{ 0.0f };
-                float maxHeightBounds{ 0.0f };
-                Physics::HeightfieldProviderRequestsBus::Event(
-                    gameEntityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldHeightBounds, minHeightBounds,
-                    maxHeightBounds);
-
-                const float halfBounds{ (maxHeightBounds - minHeightBounds) / 2.0f };
-                const float scaleFactor = (maxHeightBounds <= minHeightBounds) ? 1.0f : AZStd::numeric_limits<int16_t>::max() / halfBounds;
-
                 physx::PxHeightFieldSample samplePhysX = heightfield->getSample(sampleRow, sampleColumn);
                 Physics::HeightMaterialPoint samplePhysics = samples[sampleRow * numColumns + sampleColumn];
                 EXPECT_EQ(samplePhysX.height, azlossy_cast<physx::PxI16>(samplePhysics.m_height * scaleFactor));
@@ -339,6 +345,15 @@ namespace PhysXEditorTests
             physicsSurfaceTypes.emplace_back(materialId.GetUuid().ToString<AZStd::string>());
         }
 
+        // Our heightfield is located in the world as follows:
+        // - entity center is (0, 0)
+        // - mocked heightfield is 3 samples spaced at 1 m intervals, so it's a heightfield size of (2, 2)
+        // - mocked heightfield transform returns the heightfield center at (1, 2)
+        // - final heightfield goes from (0, 1) - (2, 3)
+        // Note: entity also has a box of size (1, 1) on it, but since we've mocked the heightfield provider, the box is ignored
+        const float heightfieldMinCornerX = 0.0f;
+        const float heightfieldMinCornerY = 1.0f;
+
         // PhysX Heightfield cooking doesn't map 1-1 sample material indices to triangle material indices 
         // Hence hardcoding the expected material indices in the test 
         const AZStd::array<int, 4> physicsMaterialsValidationDataIndex = {0, 2, 1, 1};
@@ -355,8 +370,10 @@ namespace PhysXEditorTests
 
                 if (sampleRow != numRows - 1 && sampleColumn != numColumns - 1)
                 {
-                    const float x_offset = -0.25f;
-                    const float y_offset = 0.75f;
+                    // There are two materials per quad, so we'll perform 1 raycast per triangle per quad.
+                    // Our quads are 1 m in size, so a ray at (1/4 m, 1/4 m) and (3/4 m, 3/4 m) in each quad should hit the two triangles.
+                    const float x_offset = heightfieldMinCornerX + 0.25f;
+                    const float y_offset = heightfieldMinCornerY + 0.25f;
                     const float secondRayOffset = 0.5f;
 
                     float rayX = x_offset + sampleColumn;

+ 89 - 79
Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.cpp

@@ -105,6 +105,15 @@ namespace Terrain
         services.push_back(AZ_CRC_CE("AxisAlignedBoxShapeService"));
     }
 
+    void TerrainPhysicsColliderComponent::GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& services)
+    {
+        // If any of the following appear on the same entity as this one, they should get activated first as their data will
+        // affect this component.
+        services.push_back(AZ_CRC_CE("TerrainAreaService"));
+        services.push_back(AZ_CRC_CE("TerrainHeightProviderService"));
+        services.push_back(AZ_CRC_CE("TerrainSurfaceProviderService"));
+    }
+
     void TerrainPhysicsColliderComponent::Reflect(AZ::ReflectContext* context)
     {
         TerrainPhysicsColliderConfig::Reflect(context);
@@ -147,20 +156,30 @@ namespace Terrain
         const Physics::HeightfieldProviderNotifications::HeightfieldChangeMask heightfieldChangeMask,
         const AZ::Aabb& dirtyRegion)
     {
-        AZ::Aabb worldSize = AZ::Aabb::CreateNull();
+        AZ_PROFILE_FUNCTION(Terrain);
+
+        CalculateHeightfieldRegion();
+
+        AZ::Aabb colliderBounds = GetHeightfieldAabb();
 
         if (dirtyRegion.IsValid())
         {
-            worldSize = dirtyRegion;
+            // If we have a dirty region, only update this collider if the dirty region overlaps the collider bounds.
+            if (dirtyRegion.Overlaps(colliderBounds))
+            {
+                // Find the intersection of the dirty region and the collider, and only notify about that area as changing.
+                AZ::Aabb dirtyBounds = colliderBounds.GetClamped(dirtyRegion);
+
+                Physics::HeightfieldProviderNotificationBus::Broadcast(
+                    &Physics::HeightfieldProviderNotificationBus::Events::OnHeightfieldDataChanged, dirtyBounds, heightfieldChangeMask);
+            }
         }
         else
         {
-            LmbrCentral::ShapeComponentRequestsBus::EventResult(
-                worldSize, GetEntityId(), &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb);
+            // No valid dirty region, so update the entire collider bounds.
+            Physics::HeightfieldProviderNotificationBus::Broadcast(
+                &Physics::HeightfieldProviderNotificationBus::Events::OnHeightfieldDataChanged, colliderBounds, heightfieldChangeMask);
         }
-
-        Physics::HeightfieldProviderNotificationBus::Broadcast(
-            &Physics::HeightfieldProviderNotificationBus::Events::OnHeightfieldDataChanged, worldSize, heightfieldChangeMask);
     }
 
     void TerrainPhysicsColliderComponent::OnShapeChanged([[maybe_unused]] ShapeChangeReasons changeReason)
@@ -206,38 +225,39 @@ namespace Terrain
         }
     }
 
-    AZ::Aabb TerrainPhysicsColliderComponent::GetHeightfieldAabb() const
+    void TerrainPhysicsColliderComponent::CalculateHeightfieldRegion()
     {
-        AZ::Aabb worldSize = AZ::Aabb::CreateNull();
+        AZ::Aabb heightfieldBox = AZ::Aabb::CreateNull();
 
         LmbrCentral::ShapeComponentRequestsBus::EventResult(
-            worldSize, GetEntityId(), &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb);
+            heightfieldBox, GetEntityId(), &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb);
 
-        return GetRegionClampedToGrid(worldSize);
+        const AZ::Vector2 gridResolution = GetHeightfieldGridSpacing();
+
+        AZ::Vector2 constrictedAlignedStartPoint = (AZ::Vector2(heightfieldBox.GetMin()) / gridResolution).GetCeil() * gridResolution;
+        AZ::Vector2 constrictedAlignedEndPoint = (AZ::Vector2(heightfieldBox.GetMax()) / gridResolution).GetFloor() * gridResolution;
+
+        // 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)
+        AZ::Vector2 numPoints = (constrictedAlignedEndPoint - constrictedAlignedStartPoint) / gridResolution + AZ::Vector2(1.0f);
+
+        m_heightfieldRegion.m_startPoint =
+            AZ::Vector3(constrictedAlignedStartPoint.GetX(), constrictedAlignedStartPoint.GetY(), heightfieldBox.GetMin().GetZ());
+        m_heightfieldRegion.m_stepSize = gridResolution;
+        m_heightfieldRegion.m_numPointsX = aznumeric_cast<size_t>(numPoints.GetX());
+        m_heightfieldRegion.m_numPointsY = aznumeric_cast<size_t>(numPoints.GetY());
     }
 
-    AZ::Aabb TerrainPhysicsColliderComponent::GetRegionClampedToGrid(const AZ::Aabb& region) const
+    AZ::Aabb TerrainPhysicsColliderComponent::GetHeightfieldAabb() const
     {
-        auto vector2Floor = [](const AZ::Vector2& in)
-        {
-            return AZ::Vector2(floor(in.GetX()), floor(in.GetY()));
-        };
-        auto vector2Ceil = [](const AZ::Vector2& in)
-        {
-            return AZ::Vector2(ceil(in.GetX()), ceil(in.GetY()));
-        };
-
-        const AZ::Vector2 gridResolution = GetHeightfieldGridSpacing();
-        const AZ::Vector3 boundsMin = region.GetMin();
-        const AZ::Vector3 boundsMax = region.GetMax();
+        AZ::Aabb heightfieldBox = AZ::Aabb::CreateNull();
+        LmbrCentral::ShapeComponentRequestsBus::EventResult(
+            heightfieldBox, GetEntityId(), &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb);
 
-        const AZ::Vector2 gridMinBoundLower = vector2Floor(AZ::Vector2(boundsMin) / gridResolution) * gridResolution;
-        const AZ::Vector2 gridMaxBoundUpper = vector2Ceil(AZ::Vector2(boundsMax) / gridResolution) * gridResolution;
 
-        return AZ::Aabb::CreateFromMinMaxValues(
-            gridMinBoundLower.GetX(), gridMinBoundLower.GetY(), boundsMin.GetZ(),
-            gridMaxBoundUpper.GetX(), gridMaxBoundUpper.GetY(), boundsMax.GetZ()
-        );
+        AZ::Vector3 endPoint = m_heightfieldRegion.m_startPoint +
+            AZ::Vector3(m_heightfieldRegion.m_stepSize.GetX() * (m_heightfieldRegion.m_numPointsX - 1),
+                        m_heightfieldRegion.m_stepSize.GetY() * (m_heightfieldRegion.m_numPointsY - 1), heightfieldBox.GetZExtent());
+        return AZ::Aabb::CreateFromMinMax(m_heightfieldRegion.m_startPoint, endPoint);
     }
 
     void TerrainPhysicsColliderComponent::GetHeightfieldHeightBounds(float& minHeightBounds, float& maxHeightBounds) const
@@ -269,36 +289,32 @@ namespace Terrain
     AZ::Transform TerrainPhysicsColliderComponent::GetHeightfieldTransform() const
     {
         // We currently don't support rotation of terrain heightfields.
-        AZ::Vector3 translate;
-        AZ::TransformBus::EventResult(translate, GetEntityId(), &AZ::TransformBus::Events::GetWorldTranslation);
-
-        return AZ::Transform::CreateTranslation(translate);
+        // We also need to adjust the center to account for the fact that the heightfield might be expanded unevenly from
+        // the entity's center, depending on where the entity's shape lies relative to the terrain grid.
+        return AZ::Transform::CreateTranslation(GetHeightfieldAabb().GetCenter());
     }
 
     void TerrainPhysicsColliderComponent::GenerateHeightsInBounds(AZStd::vector<float>& heights) const
     {
         AZ_PROFILE_FUNCTION(Terrain);
 
-        const AZ::Vector2 gridResolution = GetHeightfieldGridSpacing();
+        heights.clear();
+        heights.reserve(m_heightfieldRegion.m_numPointsX * m_heightfieldRegion.m_numPointsY);
 
         AZ::Aabb worldSize = GetHeightfieldAabb();
-
         const float worldCenterZ = worldSize.GetCenter().GetZ();
 
-        int32_t gridWidth, gridHeight;
-        GetHeightfieldGridSize(gridWidth, gridHeight);
-
-        heights.clear();
-        heights.reserve(gridWidth * gridHeight);
-
         auto perPositionHeightCallback = [&heights, worldCenterZ]
             ([[maybe_unused]] size_t xIndex, [[maybe_unused]] size_t yIndex, const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists)
         {
             heights.emplace_back(surfacePoint.m_position.GetZ() - worldCenterZ);
         };
 
-        AzFramework::Terrain::TerrainDataRequestBus::Broadcast(&AzFramework::Terrain::TerrainDataRequests::ProcessHeightsFromRegion,
-            worldSize, gridResolution, perPositionHeightCallback, AzFramework::Terrain::TerrainDataRequests::Sampler::DEFAULT);
+        // We can use the "EXACT" sampler here because our query points are guaranteed to be aligned with terrain grid points.
+        AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
+            &AzFramework::Terrain::TerrainDataRequests::QueryRegion, m_heightfieldRegion,
+            AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Heights,
+            perPositionHeightCallback, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT);
     }
 
     uint8_t TerrainPhysicsColliderComponent::GetMaterialIdIndex(const Physics::MaterialId& materialId, const AZStd::vector<Physics::MaterialId>& materialList) const
@@ -332,6 +348,8 @@ namespace Terrain
     void TerrainPhysicsColliderComponent::UpdateHeightsAndMaterials(
         const Physics::UpdateHeightfieldSampleFunction& updateHeightsMaterialsCallback, const AZ::Aabb& regionIn) const
     {
+        using namespace AzFramework::Terrain;
+
         AZ_PROFILE_FUNCTION(Terrain);
 
         if (!m_terrainDataActive)
@@ -346,30 +364,22 @@ namespace Terrain
         {
             region = worldSize;
         }
+        else
+        {
+            region.Clamp(worldSize);
+        }
 
         const AZ::Vector2 gridResolution = GetHeightfieldGridSpacing();
 
-        // Clamp region to world grid
-        region = GetRegionClampedToGrid(region);
-
-        size_t xOffset = 0, yOffset = 0;
-        AZ::Aabb offsetRegion = AZ::Aabb::CreateFromPoint(AZ::Vector3::CreateZero());
+        AZ::Vector2 heightfieldStartGridPoint = AZ::Vector2(m_heightfieldRegion.m_startPoint) / m_heightfieldRegion.m_stepSize;
 
-        if (region != worldSize)
-        {
-            const AZ::Vector3& worldSizeMin = worldSize.GetMin();
-            const float worldMaxZ = worldSize.GetMax().GetZ();
-            const AZ::Vector3& regionMin = region.GetMin();
-
-            offsetRegion = AZ::Aabb::CreateFromMinMaxValues(worldSizeMin.GetX(),worldSizeMin.GetY(),worldSizeMin.GetZ(),regionMin.GetX(), regionMin.GetY(), worldMaxZ);
+        AZ::Vector2 contractedAlignedStartGridPoint = (AZ::Vector2(region.GetMin()) / gridResolution).GetCeil();
+        AZ::Vector2 contractedAlignedEndGridPoint = (AZ::Vector2(region.GetMax()) / gridResolution).GetFloor();
 
-            AZStd::pair<size_t, size_t> numSamples;
-            auto sampler = AzFramework::Terrain::TerrainDataRequests::Sampler::DEFAULT;
-            AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(numSamples, &AzFramework::Terrain::TerrainDataRequests::GetNumSamplesFromRegion, offsetRegion, gridResolution, sampler);
+        AZ::Vector2 contractedAlignedStartPoint = contractedAlignedStartGridPoint * gridResolution;
 
-            xOffset = numSamples.first;
-            yOffset = numSamples.second;
-        }
+        size_t xOffset = aznumeric_cast<size_t>(contractedAlignedStartGridPoint.GetX() - heightfieldStartGridPoint.GetX());
+        size_t yOffset = aznumeric_cast<size_t>(contractedAlignedStartGridPoint.GetY() - heightfieldStartGridPoint.GetY());
 
         const float worldCenterZ = worldSize.GetCenter().GetZ();
         const float worldHeightBoundsMin = worldSize.GetMin().GetZ();
@@ -412,8 +422,19 @@ namespace Terrain
             updateHeightsMaterialsCallback(row, column, point);
         };
 
-        AzFramework::Terrain::TerrainDataRequestBus::Broadcast(&AzFramework::Terrain::TerrainDataRequests::ProcessSurfacePointsFromRegion,
-            region, gridResolution, perPositionCallback, AzFramework::Terrain::TerrainDataRequests::Sampler::DEFAULT);
+        // 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)
+        AZ::Vector2 numPoints = contractedAlignedEndGridPoint - contractedAlignedStartGridPoint + AZ::Vector2(1.0f);
+        const size_t numPointsX = AZStd::min(aznumeric_cast<size_t>(numPoints.GetX()), m_heightfieldRegion.m_numPointsX);
+        const size_t numPointsY = AZStd::min(aznumeric_cast<size_t>(numPoints.GetY()),  m_heightfieldRegion.m_numPointsY);
+        TerrainQueryRegion queryRegion(contractedAlignedStartPoint, numPointsX, numPointsY, gridResolution);
+
+        // We can use the "EXACT" sampler here because our query points are guaranteed to be aligned with terrain grid points.
+        TerrainDataRequestBus::Broadcast(
+            &TerrainDataRequests::QueryRegion,
+            queryRegion,
+            static_cast<TerrainDataRequests::TerrainDataMask>(TerrainDataRequests::TerrainDataMask::Heights | TerrainDataRequests::TerrainDataMask::SurfaceData),
+            perPositionCallback,
+            TerrainDataRequests::Sampler::EXACT);
     }
 
     void TerrainPhysicsColliderComponent::UpdateConfiguration(const TerrainPhysicsColliderConfig& newConfiguration)
@@ -435,29 +456,18 @@ namespace Terrain
 
     void TerrainPhysicsColliderComponent::GetHeightfieldGridSize(int32_t& numColumns, int32_t& numRows) const
     {
-        const AZ::Vector2 gridResolution = GetHeightfieldGridSpacing();
-        const AZ::Aabb bounds = GetHeightfieldAabb();
-
-        numColumns = aznumeric_cast<int32_t>((bounds.GetMax().GetX() - bounds.GetMin().GetX()) / gridResolution.GetX());
-        numRows = aznumeric_cast<int32_t>((bounds.GetMax().GetY() - bounds.GetMin().GetY()) / gridResolution.GetY());
+        numColumns = aznumeric_cast<int32_t>(m_heightfieldRegion.m_numPointsX);
+        numRows = aznumeric_cast<int32_t>(m_heightfieldRegion.m_numPointsY);
     }
 
     int32_t TerrainPhysicsColliderComponent::GetHeightfieldGridColumns() const
     {
-        int32_t numColumns{ 0 };
-        int32_t numRows{ 0 };
-
-        GetHeightfieldGridSize(numColumns, numRows);
-        return numColumns;
+        return aznumeric_cast<int32_t>(m_heightfieldRegion.m_numPointsX);
     }
 
     int32_t TerrainPhysicsColliderComponent::GetHeightfieldGridRows() const
     {
-        int32_t numColumns{ 0 };
-        int32_t numRows{ 0 };
-
-        GetHeightfieldGridSize(numColumns, numRows);
-        return numRows;
+        return aznumeric_cast<int32_t>(m_heightfieldRegion.m_numPointsY);
     }
 
     AZStd::vector<Physics::MaterialId> TerrainPhysicsColliderComponent::GetMaterialList() const

+ 5 - 1
Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.h

@@ -13,6 +13,7 @@
 
 #include <AzFramework/Physics/HeightfieldProviderBus.h>
 #include <AzFramework/Physics/Material.h>
+#include <AzFramework/Terrain/TerrainDataRequestBus.h>
 #include <SurfaceData/SurfaceTag.h>
 #include <TerrainSystem/TerrainSystemBus.h>
 
@@ -73,6 +74,7 @@ namespace Terrain
         static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services);
         static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& services);
         static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& services);
+        static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& services);
         static void Reflect(AZ::ReflectContext* context);
 
         TerrainPhysicsColliderComponent(const TerrainPhysicsColliderConfig& configuration);
@@ -107,7 +109,6 @@ namespace Terrain
         Physics::MaterialId FindMaterialIdForSurfaceTag(const SurfaceData::SurfaceTag tag) const;
 
         void GenerateHeightsInBounds(AZStd::vector<float>& heights) const;
-        AZ::Aabb GetRegionClampedToGrid(const AZ::Aabb& region) const;
 
         void NotifyListenersOfHeightfieldDataChange(
             Physics::HeightfieldProviderNotifications::HeightfieldChangeMask heightfieldChangeMask,
@@ -120,8 +121,11 @@ namespace Terrain
         void OnTerrainDataDestroyBegin() override;
         void OnTerrainDataChanged(const AZ::Aabb& dirtyRegion, TerrainDataChangedMask dataChangedMask) override;
 
+        void CalculateHeightfieldRegion();
+
     private:
         TerrainPhysicsColliderConfig m_configuration;
         bool m_terrainDataActive = false;
+        AzFramework::Terrain::TerrainQueryRegion m_heightfieldRegion;
     };
 }

+ 3 - 2
Gems/Terrain/Code/Source/Components/TerrainSurfaceDataSystemComponent.cpp

@@ -159,11 +159,12 @@ namespace Terrain
         size_t inPositionIndex = 0;
 
         AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-            &AzFramework::Terrain::TerrainDataRequestBus::Events::ProcessSurfacePointsFromList, inPositions,
+            &AzFramework::Terrain::TerrainDataRequestBus::Events::QueryList, inPositions,
+            AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::All,
             [this, inPositions, &inPositionIndex, &surfacePointList]
                 (const AzFramework::SurfaceData::SurfacePoint& surfacePoint, bool terrainExists)
             {
-                AZ_Assert(inPositionIndex < inPositions.size(), "Too many points returned from ProcessSurfacePointsFromList");
+                AZ_Assert(inPositionIndex < inPositions.size(), "Too many points returned from QueryList");
 
                 SurfaceData::SurfaceTagWeights weights(surfacePoint.m_surfaceTags);
 

+ 2 - 0
Gems/Terrain/Code/Source/Components/TerrainWorldComponent.cpp

@@ -94,8 +94,10 @@ namespace Terrain
                         ->Attribute(AZ::Edit::Attributes::Max, 65536.0f)
                     ->DataElement(
                         AZ::Edit::UIHandlers::Default, &TerrainWorldConfig::m_heightQueryResolution, "Height Query Resolution (m)", "")
+                        ->Attribute(AZ::Edit::Attributes::Min, 0.1f)
                     ->DataElement(
                         AZ::Edit::UIHandlers::Default, &TerrainWorldConfig::m_surfaceDataQueryResolution, "Surface Data Query Resolution (m)", "")
+                        ->Attribute(AZ::Edit::Attributes::Min, 0.1f)
                     ;
             }
         }

+ 84 - 70
Gems/Terrain/Code/Source/Components/TerrainWorldDebuggerComponent.cpp

@@ -282,16 +282,21 @@ namespace Terrain
                     AZ::Vector3(sectorX * sectorSize.GetX(), sectorY * sectorSize.GetY(), worldMinZ),
                     AZ::Vector3((sectorX + 1) * sectorSize.GetX(), (sectorY + 1) * sectorSize.GetY(), worldMinZ));
 
-                // Clamp it to the terrain world bounds.
-                sectorAabb.Clamp(worldBounds);
-
                 // If the world space box for the sector doesn't match, set it and mark the sector as dirty so we refresh the height data.
                 {
                     AZStd::lock_guard<AZStd::recursive_mutex> lock(sector.m_sectorStateMutex);
                     if (sector.m_aabb != sectorAabb)
                     {
                         sector.m_aabb = sectorAabb;
-                        sector.SetDirty();
+                        if (worldBounds.Overlaps(sector.m_aabb))
+                        {
+                            sector.SetDirty();
+                        }
+                        else
+                        {
+                            // If this sector doesn't appear in the terrain world bounds, just clear it out.
+                            sector.m_lineVertices.clear();
+                        }
                     }
                 }
             }
@@ -339,36 +344,30 @@ namespace Terrain
 
         sector.m_isDirty = false;
 
-        // To rebuild the wireframe, we walk through the sector by X, then by Y.  For each point, we add two lines in a _| shape.
-        // To do that, we'll need to cache the height from the previous point to draw the _ line, and from the previous row to draw
-        // the | line.
-
-        // When walking through the bounding box, the loops will be inclusive on one side, and exclusive on the other.  However, since
-        // our box is exactly aligned with grid points, we want to get the grid points on both sides in each direction, so we need to
-        // expand our query region by one extra point.
-        // For example, if our AABB is 2 m and our grid resolution is 1 m, we'll want to query (*--*--*--), not (*--*--).
-        // Since we're processing lines based on the grid points and going backwards, this will give us (*--*--*).
+        // To rebuild the wireframe for the sector, we grab all the sector vertex positions and whether or not that vertex has
+        // terrain data that exists.              _
+        // For each point, we add two lines in a |  shape. (inverted L)
+        // We need to query one extra point in each direction so that we can get the endpoints for the final lines in each direction.
+        AzFramework::Terrain::TerrainQueryRegion queryRegion(
+            sector.m_aabb.GetMin(), SectorSizeInGridPoints + 1, SectorSizeInGridPoints + 1, AZ::Vector2(gridResolution));
 
-        AZ::Aabb region = sector.m_aabb;
-        region.SetMax(region.GetMax() + AZ::Vector3(gridResolution, gridResolution, 0.0f));
+        const size_t numSamplesX = queryRegion.m_numPointsX;
+        const size_t numSamplesY = queryRegion.m_numPointsY;
 
-        // We need 4 vertices for each grid point in our sector to hold the _| shape.
-        const size_t numSamplesX = aznumeric_cast<size_t>(ceil(region.GetExtents().GetX() / gridResolution));
-        const size_t numSamplesY = aznumeric_cast<size_t>(ceil(region.GetExtents().GetY() / gridResolution));
+        // We need 4 vertices for each grid point in our sector to hold the inverted L shape.
         sector.m_lineVertices.clear();
-        sector.m_lineVertices.reserve(numSamplesX * numSamplesY * 4);
-
-        // This keeps track of the height from the previous point for the _ line.
-        sector.m_previousHeight = 0.0f;
-
-        // This keeps track of the heights from the previous row for the | line.
-        sector.m_rowHeights.clear();
-        sector.m_rowHeights.resize(numSamplesX);
-
-        // For each terrain height value in the region, create the _| grid lines for that point and cache off the height value
-        // for use with subsequent grid line calculations.
-        auto ProcessHeightValue = [gridResolution, &sector]
-            (size_t xIndex, size_t yIndex, const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists)
+        sector.m_lineVertices.reserve(SectorSizeInGridPoints * SectorSizeInGridPoints * 4);
+
+        // Clear and prepare our temporary buffers to hold all the vertex position data and "exists" flags.
+        // (If we're multithreading, there's no guaranteed order to which each point will get filled in)
+        sector.m_sectorVertices.clear();
+        sector.m_sectorVertexExists.clear();
+        sector.m_sectorVertices.resize(numSamplesX * numSamplesY);
+        sector.m_sectorVertexExists.resize(numSamplesX * numSamplesY);
+
+        // Cache off the vertex position data and "exists" flags.
+        auto ProcessHeightValue = [numSamplesX, &sector]
+            (size_t xIndex, size_t yIndex, const AzFramework::SurfaceData::SurfacePoint& surfacePoint, bool terrainExists)
         {
             AZStd::lock_guard<AZStd::recursive_mutex> lock(sector.m_sectorStateMutex);
             if (sector.m_isDirty)
@@ -377,50 +376,65 @@ namespace Terrain
                 return;
             }
 
-            // Don't add any vertices for the first column or first row.  These grid lines will be handled by an adjacent sector, if
-            // there is one.
-            if ((xIndex > 0) && (yIndex > 0))
-            {
-                float x = surfacePoint.m_position.GetX() - gridResolution;
-                float y = surfacePoint.m_position.GetY() - gridResolution;
-
-                sector.m_lineVertices.emplace_back(AZ::Vector3(x, surfacePoint.m_position.GetY(), sector.m_previousHeight));
-                sector.m_lineVertices.emplace_back(surfacePoint.m_position);
-
-                sector.m_lineVertices.emplace_back(AZ::Vector3(surfacePoint.m_position.GetX(), y, sector.m_rowHeights[xIndex]));
-                sector.m_lineVertices.emplace_back(surfacePoint.m_position);
-            }
-
-            // Save off the heights so that we can use them to draw subsequent columns and rows.
-            sector.m_previousHeight = surfacePoint.m_position.GetZ();
-            sector.m_rowHeights[xIndex] = surfacePoint.m_position.GetZ();
+            sector.m_sectorVertices[(yIndex * numSamplesX) + xIndex] = surfacePoint.m_position;
+            sector.m_sectorVertexExists[(yIndex * numSamplesX) + xIndex] = terrainExists;
         };
 
-        auto completionCallback = [&sector](AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext>)
+        // When we've finished gathering all the height data, create all the wireframe lines.
+        auto completionCallback =
+            [&sector, numSamplesX, numSamplesY](AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext>)
         {
-            // This must happen outside the lock,
-            // otherwise we will get a deadlock if
-            // WireframeSector::Reset is waiting for
-            // the completion event to be signalled.
+            // This must happen outside the lock, otherwise we will get a deadlock if
+            // WireframeSector::Reset is waiting for the completion event to be signalled.
             sector.m_jobCompletionEvent->release();
 
             // Reset the job context once the async request has completed,
             // clearing the way for future requests to be made for this sector.
             AZStd::lock_guard<AZStd::recursive_mutex> lock(sector.m_sectorStateMutex);
             sector.m_jobContext.reset();
+
+            // For each vertex in the sector, try to create the inverted L shape. We'll only draw a wireframe line
+            // if both the start and the end vertex has terrain data.
+            for (size_t yIndex = 0; yIndex < (numSamplesY - 1); yIndex++)
+            {
+                for (size_t xIndex = 0; xIndex < (numSamplesX - 1); xIndex++)
+                {
+                    size_t curIndex = (yIndex * numSamplesX) + xIndex;
+                    size_t rightIndex = (yIndex * numSamplesX) + xIndex + 1;
+                    size_t bottomIndex = ((yIndex + 1) * numSamplesX) + xIndex;
+
+                    if (sector.m_sectorVertexExists[curIndex] && sector.m_sectorVertexExists[bottomIndex])
+                    {
+                        sector.m_lineVertices.emplace_back(sector.m_sectorVertices[curIndex]);
+                        sector.m_lineVertices.emplace_back(sector.m_sectorVertices[bottomIndex]);
+                    }
+
+                    if (sector.m_sectorVertexExists[curIndex] && sector.m_sectorVertexExists[rightIndex])
+                    {
+                        sector.m_lineVertices.emplace_back(sector.m_sectorVertices[curIndex]);
+                        sector.m_lineVertices.emplace_back(sector.m_sectorVertices[rightIndex]);
+                    }
+                }
+            }
+
+            // We're done with our temporary height buffers so clear them back out.
+            sector.m_sectorVertices.clear();
+            sector.m_sectorVertexExists.clear();
         };
 
-        AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams> asyncParams
-            = AZStd::make_shared<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams>();
+        AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> asyncParams
+            = AZStd::make_shared<AzFramework::Terrain::QueryAsyncParams>();
         asyncParams->m_completionCallback = completionCallback;
 
+        // Only allow one thread per sector because we'll likely have multiple sectors processing at once.
+        asyncParams->m_desiredNumberOfJobs = 1;
+
+        // We can use an "EXACT" sampler here because our points are guaranteed to be aligned with terrain grid points.
         sector.m_jobCompletionEvent = AZStd::make_unique<AZStd::semaphore>();
-        AZ::Vector2 stepSize = AZ::Vector2(gridResolution);
         AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
-            sector.m_jobContext,
-            &AzFramework::Terrain::TerrainDataRequests::ProcessHeightsFromRegionAsync,
-            region,
-            stepSize,
+            sector.m_jobContext, &AzFramework::Terrain::TerrainDataRequests::QueryRegionAsync,
+            queryRegion,
+            AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Heights,
             ProcessHeightValue,
             AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT,
             asyncParams);
@@ -448,8 +462,8 @@ namespace Terrain
         m_jobContext = other.m_jobContext;
         m_aabb = other.m_aabb;
         m_lineVertices = other.m_lineVertices;
-        m_rowHeights = other.m_rowHeights;
-        m_previousHeight = other.m_previousHeight;
+        m_sectorVertices = other.m_sectorVertices;
+        m_sectorVertexExists = other.m_sectorVertexExists;
         m_isDirty = other.m_isDirty;
     }
 
@@ -459,8 +473,8 @@ namespace Terrain
         m_jobContext = AZStd::move(other.m_jobContext);
         m_aabb = AZStd::move(other.m_aabb);
         m_lineVertices = AZStd::move(other.m_lineVertices);
-        m_rowHeights = AZStd::move(other.m_rowHeights);
-        m_previousHeight = AZStd::move(other.m_previousHeight);
+        m_sectorVertices = AZStd::move(other.m_sectorVertices);
+        m_sectorVertexExists = AZStd::move(other.m_sectorVertexExists);
         m_isDirty = AZStd::move(other.m_isDirty);
     }
 
@@ -470,8 +484,8 @@ namespace Terrain
         m_jobContext = other.m_jobContext;
         m_aabb = other.m_aabb;
         m_lineVertices = other.m_lineVertices;
-        m_rowHeights = other.m_rowHeights;
-        m_previousHeight = other.m_previousHeight;
+        m_sectorVertices = other.m_sectorVertices;
+        m_sectorVertexExists = other.m_sectorVertexExists;
         m_isDirty = other.m_isDirty;
         return *this;
     }
@@ -482,8 +496,8 @@ namespace Terrain
         m_jobContext = AZStd::move(other.m_jobContext);
         m_aabb = AZStd::move(other.m_aabb);
         m_lineVertices = AZStd::move(other.m_lineVertices);
-        m_rowHeights = AZStd::move(other.m_rowHeights);
-        m_previousHeight = AZStd::move(other.m_previousHeight);
+        m_sectorVertices = AZStd::move(other.m_sectorVertices);
+        m_sectorVertexExists = AZStd::move(other.m_sectorVertexExists);
         m_isDirty = AZStd::move(other.m_isDirty);
         return *this;
     }
@@ -501,8 +515,8 @@ namespace Terrain
         }
         m_aabb = AZ::Aabb::CreateNull();
         m_lineVertices.clear();
-        m_rowHeights.clear();
-        m_previousHeight = 0.0f;
+        m_sectorVertices.clear();
+        m_sectorVertexExists.clear();
         m_isDirty = true;
     }
 

+ 3 - 3
Gems/Terrain/Code/Source/Components/TerrainWorldDebuggerComponent.h

@@ -100,13 +100,13 @@ namespace Terrain
             // This should only be called within the scope of a lock on m_sectorStateMutex.
             void SetDirty();
 
-            AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext> m_jobContext;
+            AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> m_jobContext;
             AZStd::unique_ptr<AZStd::semaphore> m_jobCompletionEvent;
             AZStd::recursive_mutex m_sectorStateMutex;
             AZ::Aabb m_aabb{ AZ::Aabb::CreateNull() };
             AZStd::vector<AZ::Vector3> m_lineVertices;
-            AZStd::vector<float> m_rowHeights;
-            float m_previousHeight = 0.0f;
+            AZStd::vector<AZ::Vector3> m_sectorVertices;
+            AZStd::vector<bool> m_sectorVertexExists;
             bool m_isDirty{ true };
         };
 

+ 9 - 0
Gems/Terrain/Code/Source/EditorComponents/EditorTerrainPhysicsColliderComponent.cpp

@@ -89,6 +89,15 @@ namespace Terrain
         services.push_back(AZ_CRC_CE("AxisAlignedBoxShapeService"));
     }
 
+    void EditorTerrainPhysicsColliderComponent::GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& services)
+    {
+        // If any of the following appear on the same entity as this one, they should get activated first as their data will
+        // affect this component.
+        services.push_back(AZ_CRC_CE("TerrainAreaService"));
+        services.push_back(AZ_CRC_CE("TerrainHeightProviderService"));
+        services.push_back(AZ_CRC_CE("TerrainSurfaceProviderService"));
+    }
+
     void EditorTerrainPhysicsColliderComponent::Init()
     {
         m_component.Init();

+ 1 - 0
Gems/Terrain/Code/Source/EditorComponents/EditorTerrainPhysicsColliderComponent.h

@@ -26,6 +26,7 @@ namespace Terrain
         static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services);
         static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& services);
         static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& services);
+        static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& services);
 
         // AZ::Component interface implementation
         void Init() override;

+ 4 - 2
Gems/Terrain/Code/Source/TerrainRenderer/TerrainDetailMaterialManager.cpp

@@ -955,9 +955,11 @@ namespace Terrain
         AZ::Vector2 stepSize(m_detailTextureScale);
         AZ::Aabb offsetWorldAabb = worldUpdateAabb.GetTranslated(AZ::Vector3(m_detailTextureScale * 0.5f)); // offset by half a pixel
 
+        AzFramework::Terrain::TerrainQueryRegion queryRegion(offsetWorldAabb.GetMin(), width, height, stepSize);
         AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-            &AzFramework::Terrain::TerrainDataRequests::ProcessSurfaceWeightsFromRegion, offsetWorldAabb, stepSize, perPositionCallback,
-            AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT);
+            &AzFramework::Terrain::TerrainDataRequests::QueryRegion, queryRegion,
+            AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::SurfaceData, perPositionCallback,
+            AzFramework::Terrain::TerrainDataRequests::Sampler::DEFAULT);
 
         const int32_t left = textureUpdateAabb.m_min.m_x;
         const int32_t top = textureUpdateAabb.m_min.m_y;

+ 12 - 24
Gems/Terrain/Code/Source/TerrainRenderer/TerrainMeshManager.cpp

@@ -572,33 +572,11 @@ namespace Terrain
 
     void TerrainMeshManager::GatherMeshData(SectorDataRequest request, AZStd::vector<HeightDataType>& meshHeights, AZStd::vector<NormalDataType>& meshNormals, AZ::Aabb& meshAabb)
     {
-        AZ::Vector3 aabbMin = AZ::Vector3(request.m_worldStartPosition.GetX(), request.m_worldStartPosition.GetY(), m_worldBounds.GetMin().GetZ());
-        AZ::Vector3 aabbMax = aabbMin + AZ::Vector3(request.m_gridSize * request.m_vertexSpacing);
-
-        // expand the bounds in order to calculate normals.
-        AZ::Vector3 queryAabbMin = aabbMin - AZ::Vector3(request.m_vertexSpacing);
-        AZ::Vector3 queryAabbMax = aabbMax + AZ::Vector3(request.m_vertexSpacing * 2.0f); // extra padding to catch the last vertex
-
-        // pad the max by half a sample spacing to make sure it's inclusive of the last point.
-        AZ::Aabb queryBounds = AZ::Aabb::CreateFromMinMax(queryAabbMin, queryAabbMax);
-
-        auto samplerType = AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP;
         const AZ::Vector2 stepSize(request.m_vertexSpacing);
 
-        AZStd::pair<size_t, size_t> numSamples;
-        AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
-            numSamples, &AzFramework::Terrain::TerrainDataRequests::GetNumSamplesFromRegion,
-            queryBounds, stepSize, samplerType);
-
         uint16_t vertexCount1d = (request.m_gridSize + 1); // grid size is length, need an extra vertex in each dimension to draw the final row / column of quads.
         uint16_t paddedVertexCount1d = vertexCount1d + 2; // extra row / column on each side for normals.
 
-        if (numSamples.first != paddedVertexCount1d || numSamples.second != paddedVertexCount1d)
-        {
-            AZ_Assert(false, "Number of samples returned from GetNumSamplesFromRegion does not match expectations");
-            return;
-        }
-
         uint16_t meshVertexCount = vertexCount1d * vertexCount1d;
         uint16_t queryVertexCount = paddedVertexCount1d * paddedVertexCount1d;
         AZStd::vector<float> heights;
@@ -614,9 +592,15 @@ namespace Terrain
             heights.at(yIndex * paddedVertexCount1d + xIndex) = height;
         };
 
+        AzFramework::Terrain::TerrainQueryRegion queryRegion(
+            request.m_worldStartPosition - stepSize, paddedVertexCount1d, paddedVertexCount1d, stepSize);
+
         AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-            &AzFramework::Terrain::TerrainDataRequests::ProcessHeightsFromRegion,
-            queryBounds, stepSize, perPositionCallback, samplerType);
+            &AzFramework::Terrain::TerrainDataRequests::QueryRegion,
+            queryRegion,
+            AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Heights,
+            perPositionCallback,
+            AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP);
 
         const float rcpWorldZ = 1.0f / m_worldBounds.GetExtents().GetZ();
         const float vertexSpacing2 = request.m_vertexSpacing * 2.0f;
@@ -671,6 +655,10 @@ namespace Terrain
             }
         }
 
+
+        AZ::Vector3 aabbMin =
+            AZ::Vector3(request.m_worldStartPosition.GetX(), request.m_worldStartPosition.GetY(), m_worldBounds.GetMin().GetZ());
+        AZ::Vector3 aabbMax = aabbMin + AZ::Vector3(request.m_gridSize * request.m_vertexSpacing);
         aabbMin.SetZ(minHeight);
         aabbMax.SetZ(maxHeight);
         meshAabb.Set(aabbMin, aabbMax);

+ 211 - 405
Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp

@@ -261,33 +261,21 @@ void TerrainSystem::GenerateQueryPositions(const AZStd::span<const AZ::Vector3>&
 }
 
 AZStd::vector<AZ::Vector3> TerrainSystem::GenerateInputPositionsFromRegion(
-    const AZ::Aabb& inRegion,
-    const AZ::Vector2& stepSize,
-    Sampler sampler) const
+    const AzFramework::Terrain::TerrainQueryRegion& queryRegion) const
 {
     AZ_PROFILE_FUNCTION(Terrain);
 
     AZStd::vector<AZ::Vector3> inPositions;
-    const auto [numSamplesX, numSamplesY] = GetNumSamplesFromRegion(inRegion, stepSize, sampler);
-    inPositions.reserve(numSamplesX * numSamplesY);
+    inPositions.reserve(queryRegion.m_numPointsX * queryRegion.m_numPointsY);
 
-    AZ::Vector2 startPosition = AZ::Vector2(inRegion.GetMin().GetX(), inRegion.GetMin().GetY());
+    AZ::Vector2 startPosition(queryRegion.m_startPoint);
 
-    if (sampler == Sampler::CLAMP)
+    for (size_t y = 0; y < queryRegion.m_numPointsY; y++)
     {
-        // Adjust the start position to be on a clamped position.
-        const int32_t firstSampleX = aznumeric_cast<int32_t>(AZStd::ceilf(inRegion.GetMin().GetX() / stepSize.GetX()));
-        const int32_t firstSampleY = aznumeric_cast<int32_t>(AZStd::ceilf(inRegion.GetMin().GetY() / stepSize.GetY()));
-        startPosition.SetX(firstSampleX * stepSize.GetX());
-        startPosition.SetY(firstSampleY * stepSize.GetY());
-    }
-
-    for (size_t y = 0; y < numSamplesY; y++)
-    {
-        float fy = aznumeric_cast<float>(inRegion.GetMin().GetY() + (y * stepSize.GetY()));
-        for (size_t x = 0; x < numSamplesX; x++)
+        float fy = aznumeric_cast<float>(queryRegion.m_startPoint.GetY() + (y * queryRegion.m_stepSize.GetY()));
+        for (size_t x = 0; x < queryRegion.m_numPointsX; x++)
         {
-            float fx = aznumeric_cast<float>(inRegion.GetMin().GetX() + (x * stepSize.GetX()));
+            float fx = aznumeric_cast<float>(queryRegion.m_startPoint.GetX() + (x * queryRegion.m_stepSize.GetX()));
             inPositions.emplace_back(AZ::Vector3(fx, fy, 0.0f));
         }
     }
@@ -760,128 +748,146 @@ AzFramework::RenderGeometry::RayResult TerrainSystem::GetClosestIntersection(
     return m_terrainRaycastContext.RayIntersect(ray);
 }
 
-AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext> TerrainSystem::ProcessHeightsFromListAsync(
+AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> TerrainSystem::QueryListAsync(
     const AZStd::span<const AZ::Vector3>& inPositions,
+    TerrainDataMask requestedData,
     AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
     Sampler sampler,
-    AZStd::shared_ptr<ProcessAsyncParams> params) const
+    AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> params) const
 {
-    return ProcessFromListAsync(AZStd::bind(&TerrainSystem::ProcessHeightsFromList, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3),
-        inPositions, perPositionCallback, sampler, params);
+    return ProcessFromListAsync(inPositions, requestedData, perPositionCallback, sampler, params);
 }
 
-AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext> TerrainSystem::ProcessNormalsFromListAsync(
-    const AZStd::span<const AZ::Vector3>& inPositions,
+AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> TerrainSystem::QueryListOfVector2Async(
+    const AZStd::span<const AZ::Vector2>& inPositions,
+    TerrainDataMask requestedData,
     AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
     Sampler sampler,
-    AZStd::shared_ptr<ProcessAsyncParams> params) const
+    AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> params) const
 {
-    return ProcessFromListAsync(AZStd::bind(&TerrainSystem::ProcessNormalsFromList, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3),
-        inPositions, perPositionCallback, sampler, params);
+    return ProcessFromListAsync(inPositions, requestedData, perPositionCallback, sampler, params);
 }
 
-AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext> TerrainSystem::ProcessSurfaceWeightsFromListAsync(
-    const AZStd::span<const AZ::Vector3>& inPositions,
-    AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
+AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> TerrainSystem::QueryRegionAsync(
+    const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
+    TerrainDataMask requestedData,
+    AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
     Sampler sampler,
-    AZStd::shared_ptr<ProcessAsyncParams> params) const
+    AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> params) const
 {
-    return ProcessFromListAsync(AZStd::bind(&TerrainSystem::ProcessSurfaceWeightsFromList, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3),
-        inPositions, perPositionCallback, sampler, params);
-}
+    AZ_PROFILE_FUNCTION(Terrain);
 
-AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext> TerrainSystem::ProcessSurfacePointsFromListAsync(
-    const AZStd::span<const AZ::Vector3>& inPositions,
-    AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
-    Sampler sampler,
-    AZStd::shared_ptr<ProcessAsyncParams> params) const
-{
-    return ProcessFromListAsync(AZStd::bind(&TerrainSystem::ProcessSurfacePointsFromList, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3),
-        inPositions, perPositionCallback, sampler, params);
-}
+    auto numSamplesX = queryRegion.m_numPointsX;
+    auto numSamplesY = queryRegion.m_numPointsY;
+    const int64_t numPositionsToProcess = numSamplesX * numSamplesY;
 
-AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext> TerrainSystem::ProcessHeightsFromListOfVector2Async(
-    const AZStd::span<const AZ::Vector2>& inPositions,
-    AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
-    Sampler sampler,
-    AZStd::shared_ptr<ProcessAsyncParams> params) const
-{
-    return ProcessFromListAsync(AZStd::bind(&TerrainSystem::ProcessHeightsFromListOfVector2, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3),
-        inPositions, perPositionCallback, sampler, params);
-}
+    if (numPositionsToProcess == 0)
+    {
+        AZ_Warning("TerrainSystem", false, "No positions to process.");
+        return nullptr;
+    }
 
-AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext> TerrainSystem::ProcessNormalsFromListOfVector2Async(
-    const AZStd::span<const AZ::Vector2>& inPositions,
-    AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
-    Sampler sampler,
-    AZStd::shared_ptr<ProcessAsyncParams> params) const
-{
-    return ProcessFromListAsync(AZStd::bind(&TerrainSystem::ProcessNormalsFromListOfVector2, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3),
-        inPositions, perPositionCallback, sampler, params);
-}
+    // Determine the maximum number of jobs, and the minimum number of positions that should be processed per job.
+    const int32_t numJobsMax = CalculateMaxJobs(params);
+    const int32_t minPositionsPerJob = params && (params->m_minPositionsPerJob > 0)
+        ? params->m_minPositionsPerJob
+        : AzFramework::Terrain::QueryAsyncParams::MinPositionsPerJobDefault;
+
+    // Calculate the best subdivision of the region along both the X and Y axes to use as close to the maximum number of jobs
+    // as possible while also keeping all the regions effectively the same size.
+    int32_t xJobs, yJobs;
+    SubdivideRegionForJobs(
+        aznumeric_cast<int32_t>(numSamplesX), aznumeric_cast<int32_t>(numSamplesY), numJobsMax, minPositionsPerJob, xJobs, yJobs);
+
+    // The number of jobs returned might be less than the total requested maximum number of jobs, so recalculate it here
+    const int32_t numJobs = xJobs * yJobs;
+
+    // Get the number of samples in each direction that we'll use for each query. We calculate this as a fractional value
+    // so that we can keep each query pretty evenly balanced, with just +/- 1 count variation on each axis.
+    const float xSamplesPerQuery = aznumeric_cast<float>(numSamplesX) / xJobs;
+    const float ySamplesPerQuery = aznumeric_cast<float>(numSamplesY) / yJobs;
+
+    // Make sure our subdivisions are producing at least minPositionsPerJob unless the *total* requested point count is
+    // less than minPositionsPerJob.
+    AZ_Assert(
+        ((numSamplesX * numSamplesY) < minPositionsPerJob) ||
+        (aznumeric_cast<int32_t>(xSamplesPerQuery) * aznumeric_cast<int32_t>(ySamplesPerQuery)) >= minPositionsPerJob,
+        "Too few positions per job: %d vs %d", aznumeric_cast<int32_t>(xSamplesPerQuery) * aznumeric_cast<int32_t>(ySamplesPerQuery),
+        minPositionsPerJob);
+
+    // Create a terrain job context and split the work across multiple jobs.
+    AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> jobContext =
+        AZStd::make_shared<AzFramework::Terrain::TerrainJobContext>(*m_terrainJobManager, numJobs);
+    {
+        AZStd::unique_lock<AZStd::mutex> lock(m_activeTerrainJobContextMutex);
+        m_activeTerrainJobContexts.push_back(jobContext);
+    }
 
-AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext> TerrainSystem::ProcessSurfaceWeightsFromListOfVector2Async(
-    const AZStd::span<const AZ::Vector2>& inPositions,
-    AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
-    Sampler sampler,
-    AZStd::shared_ptr<ProcessAsyncParams> params) const
-{
-    return ProcessFromListAsync(AZStd::bind(&TerrainSystem::ProcessSurfaceWeightsFromListOfVector2, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3),
-        inPositions, perPositionCallback, sampler, params);
-}
+    [[maybe_unused]] int32_t jobsStarted = 0;
 
-AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext> TerrainSystem::ProcessSurfacePointsFromListOfVector2Async(
-    const AZStd::span<const AZ::Vector2>& inPositions,
-    AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
-    Sampler sampler,
-    AZStd::shared_ptr<ProcessAsyncParams> params) const
-{
-    return ProcessFromListAsync(AZStd::bind(&TerrainSystem::ProcessSurfacePointsFromListOfVector2, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3),
-        inPositions, perPositionCallback, sampler, params);
-}
+    for (int32_t yJob = 0; yJob < yJobs; yJob++)
+    {
+        // Use the fractional samples per query to calculate the start and end of the region, but then convert it
+        // back to integers so that our regions are always in exact multiples of the number of samples to process.
+        // This is important because we want the XY values for each point that we're processing to exactly align with
+        // 'start + N * (step size)', or else we'll start to process point locations that weren't actually what was requested.
+        const int32_t y0 = aznumeric_cast<int32_t>(yJob * ySamplesPerQuery);
+        const int32_t y1 = aznumeric_cast<int32_t>((yJob + 1) * ySamplesPerQuery);
+        const float inRegionMinY = queryRegion.m_startPoint.GetY() + (y0 * queryRegion.m_stepSize.GetY());
+        const int32_t numPointsY = AZStd::min(y1 - y0, aznumeric_cast<int32_t>(numSamplesY) - y0);
 
-AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext> TerrainSystem::ProcessHeightsFromRegionAsync(
-    const AZ::Aabb& inRegion,
-    const AZ::Vector2& stepSize,
-    AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
-    Sampler sampler,
-    AZStd::shared_ptr<ProcessAsyncParams> params) const
-{
-    return ProcessFromRegionAsync(AZStd::bind(&TerrainSystem::ProcessHeightsFromRegion, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4),
-        inRegion, stepSize, perPositionCallback, sampler, params);
-}
 
-AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext> TerrainSystem::ProcessNormalsFromRegionAsync(
-    const AZ::Aabb& inRegion,
-    const AZ::Vector2& stepSize,
-    AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
-    Sampler sampler,
-    AZStd::shared_ptr<ProcessAsyncParams> params) const
-{
-    return ProcessFromRegionAsync(AZStd::bind(&TerrainSystem::ProcessNormalsFromRegion, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4),
-        inRegion, stepSize, perPositionCallback, sampler, params);
-}
+        for (int32_t xJob = 0; xJob < xJobs; xJob++)
+        {
+            // Same as above, calculate the start and end of the region, then convert back to integers and create the
+            // region based on 'start + n * (step size)'.
+            const int32_t x0 = aznumeric_cast<int32_t>(xJob * xSamplesPerQuery);
+            const int32_t x1 = aznumeric_cast<int32_t>((xJob + 1) * xSamplesPerQuery);
+            const float inRegionMinX = queryRegion.m_startPoint.GetX() + (x0 * queryRegion.m_stepSize.GetX());
+            const int32_t numPointsX = AZStd::min(x1 - x0, aznumeric_cast<int32_t>(numSamplesX) - x0);
+
+            // Define the job function using the sub region of positions to process.
+            AzFramework::Terrain::TerrainQueryRegion subQueryRegion(
+                AZ::Vector3(inRegionMinX, inRegionMinY, queryRegion.m_startPoint.GetZ()), numPointsX, numPointsY, queryRegion.m_stepSize);
+
+            auto jobFunction = [this, subQueryRegion, x0, y0, requestedData, perPositionCallback, sampler, jobContext, params]()
+            {
+                // Process the sub region of positions, unless the associated job context has been cancelled.
+                if (!jobContext->IsCancelled())
+                {
+                    QueryRegionInternal(subQueryRegion, x0, y0, requestedData, perPositionCallback, sampler);
+                }
 
-AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext> TerrainSystem::ProcessSurfaceWeightsFromRegionAsync(
-    const AZ::Aabb& inRegion,
-    const AZ::Vector2& stepSize,
-    AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
-    Sampler sampler,
-    AZStd::shared_ptr<ProcessAsyncParams> params) const
-{
-    return ProcessFromRegionAsync(AZStd::bind(&TerrainSystem::ProcessSurfaceWeightsFromRegion, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4),
-        inRegion, stepSize, perPositionCallback, sampler, params);
-}
+                // Decrement the number of completions remaining, invoke the completion callback if this happens
+                // to be the final job completed, and remove this TerrainJobContext from the list of active ones.
+                const bool wasLastJobCompleted = jobContext->OnJobCompleted();
+                if (wasLastJobCompleted)
+                {
+                    if (params && params->m_completionCallback)
+                    {
+                        params->m_completionCallback(jobContext);
+                    }
+
+                    {
+                        AZStd::unique_lock<AZStd::mutex> lock(m_activeTerrainJobContextMutex);
+                        m_activeTerrainJobContexts.erase(
+                            AZStd::find(m_activeTerrainJobContexts.begin(), m_activeTerrainJobContexts.end(), jobContext));
+                        m_activeTerrainJobContextMutexConditionVariable.notify_one();
+                    }
+                }
+            };
 
-AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext> TerrainSystem::ProcessSurfacePointsFromRegionAsync(
-    const AZ::Aabb& inRegion,
-            const AZ::Vector2& stepSize,
-            AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
-            Sampler sampler,
-            AZStd::shared_ptr<ProcessAsyncParams> params) const
-{
-    return ProcessFromRegionAsync(AZStd::bind(&TerrainSystem::ProcessSurfacePointsFromRegion, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4),
-        inRegion, stepSize, perPositionCallback, sampler, params);
+            // Create the job and start it immediately.
+            AZ::Job* processJob = AZ::CreateJobFunction(jobFunction, true, jobContext.get());
+            processJob->Start();
+            jobsStarted++;
+        }
+    }
+
+    // Validate this just to ensure that the fractional math for handling points didn't cause any rounding errors anywhere.
+    AZ_Assert(jobsStarted == numJobs, "Wrong number of jobs created: %d vs %d", jobsStarted, numJobs);
+
+    return jobContext;
 }
 
 AZ::EntityId TerrainSystem::FindBestAreaEntityAtPosition(const AZ::Vector3& position, AZ::Aabb& bounds) const
@@ -1011,8 +1017,9 @@ const char* TerrainSystem::GetMaxSurfaceName(
     return "";
 }
 
-void TerrainSystem::ProcessHeightsFromList(
+void TerrainSystem::QueryList(
     const AZStd::span<const AZ::Vector3>& inPositions,
+    TerrainDataMask requestedData,
     AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
     Sampler sampler) const
 {
@@ -1024,159 +1031,54 @@ void TerrainSystem::ProcessHeightsFromList(
     }
 
     AZStd::vector<bool> terrainExists(inPositions.size());
-    AZStd::vector<float> heights(inPositions.size());
-
-    GetHeightsSynchronous(inPositions, sampler, heights, terrainExists);
+    AZStd::vector<float> heights;
+    AZStd::vector<AZ::Vector3> normals;
+    AZStd::vector<AzFramework::SurfaceData::SurfaceTagWeightList> surfaceWeights;
 
-    AzFramework::SurfaceData::SurfacePoint surfacePoint;
-    for (size_t i = 0; i < inPositions.size(); i++)
+    if (requestedData & TerrainDataMask::Heights)
     {
-        surfacePoint.m_position = inPositions[i];
-        surfacePoint.m_position.SetZ(heights[i]);
-        perPositionCallback(surfacePoint, terrainExists[i]);
+        heights.resize(inPositions.size());
+        GetHeightsSynchronous(inPositions, sampler, heights, terrainExists);
     }
-}
-
-void TerrainSystem::ProcessNormalsFromList(
-    const AZStd::span<const AZ::Vector3>& inPositions,
-    AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
-    Sampler sampler) const
-{
-    AZ_PROFILE_FUNCTION(Terrain);
-
-    if (!perPositionCallback)
+    if (requestedData & TerrainDataMask::Normals)
     {
-        return;
+        normals.resize(inPositions.size());
+        GetNormalsSynchronous(inPositions, sampler, normals, terrainExists);
     }
-
-    AZStd::vector<bool> terrainExists(inPositions.size());
-    AZStd::vector<AZ::Vector3> normals(inPositions.size());
-
-    GetNormalsSynchronous(inPositions, sampler, normals, terrainExists);
-
-    AzFramework::SurfaceData::SurfacePoint surfacePoint;
-    for (size_t i = 0; i < inPositions.size(); i++)
+    if (requestedData & TerrainDataMask::SurfaceData)
     {
-        surfacePoint.m_position = inPositions[i];
-        surfacePoint.m_normal = AZStd::move(normals[i]);
-        perPositionCallback(surfacePoint, terrainExists[i]);
-    }
-}
+        // We can potentially skip an extra call to GetHeights if we already
+        // got the terrain exists flags in the earlier call to GetHeights
+        AZStd::vector<bool> terrainExistsEmpty;
 
-void TerrainSystem::ProcessSurfaceWeightsFromList(
-    const AZStd::span<const AZ::Vector3>& inPositions,
-    AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
-    Sampler sampler) const
-{
-    AZ_PROFILE_FUNCTION(Terrain);
-
-    if (!perPositionCallback)
-    {
-        return;
+        surfaceWeights.resize(inPositions.size());
+        GetOrderedSurfaceWeightsFromList(inPositions, sampler, surfaceWeights,
+            (requestedData & TerrainDataMask::Heights) ? terrainExistsEmpty : terrainExists);
     }
 
-    AZStd::vector<AzFramework::SurfaceData::SurfaceTagWeightList> outSurfaceWeightsList(inPositions.size());
-    AZStd::vector<bool> terrainExists(inPositions.size());
-
-    GetOrderedSurfaceWeightsFromList(inPositions, sampler, outSurfaceWeightsList, terrainExists);
-
     AzFramework::SurfaceData::SurfacePoint surfacePoint;
     for (size_t i = 0; i < inPositions.size(); i++)
     {
         surfacePoint.m_position = inPositions[i];
-        surfacePoint.m_surfaceTags = AZStd::move(outSurfaceWeightsList[i]);
-        perPositionCallback(surfacePoint, terrainExists[i]);
-    }
-}
-
-void TerrainSystem::ProcessSurfacePointsFromList(
-    const AZStd::span<const AZ::Vector3>& inPositions,
-    AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
-    Sampler sampler) const
-{
-    AZ_PROFILE_FUNCTION(Terrain);
-
-    if (!perPositionCallback)
-    {
-        return;
-    }
-
-    AZStd::vector<float> heights(inPositions.size());
-    AZStd::vector<AZ::Vector3> normals(inPositions.size());
-    AZStd::vector<AzFramework::SurfaceData::SurfaceTagWeightList> outSurfaceWeightsList(inPositions.size());
-    AZStd::vector<bool> terrainExists(inPositions.size());
-
-    GetHeightsSynchronous(inPositions, sampler, heights, terrainExists);
-    GetNormalsSynchronous(inPositions, sampler, normals, terrainExists);
-
-    // We can skip the unnecessary call to GetHeights since we already
-    // got the terrain exists flags in the earlier call to GetHeights
-    AZStd::vector<bool> terrainExistsEmpty;
-    GetOrderedSurfaceWeightsFromList(inPositions, sampler, outSurfaceWeightsList, terrainExistsEmpty);
-
-    AzFramework::SurfaceData::SurfacePoint surfacePoint;
-    for (size_t i = 0; i < inPositions.size(); i++)
-    {
-        surfacePoint.m_position.Set(inPositions[i].GetX(), inPositions[i].GetY(), heights[i]);
-        surfacePoint.m_normal = AZStd::move(normals[i]);
-        surfacePoint.m_surfaceTags = AZStd::move(outSurfaceWeightsList[i]);
+        if (requestedData & TerrainDataMask::Heights)
+        {
+            surfacePoint.m_position.SetZ(heights[i]);
+        }
+        if (requestedData & TerrainDataMask::Normals)
+        {
+            surfacePoint.m_normal = AZStd::move(normals[i]);
+        }
+        if (requestedData & TerrainDataMask::SurfaceData)
+        {
+            surfacePoint.m_surfaceTags = AZStd::move(surfaceWeights[i]);
+        }
         perPositionCallback(surfacePoint, terrainExists[i]);
     }
 }
 
-void TerrainSystem::ProcessHeightsFromListOfVector2(
-    const AZStd::span<const AZ::Vector2>& inPositions,
-    AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
-    Sampler sampler) const
-{
-    AZ_PROFILE_FUNCTION(Terrain);
-
-    if (!perPositionCallback)
-    {
-        return;
-    }
-
-    AZStd::vector<AZ::Vector3> inPositionsVec3 = GenerateInputPositionsFromListOfVector2(inPositions);
-
-    ProcessHeightsFromList(inPositionsVec3, perPositionCallback, sampler);
-}
-
-void TerrainSystem::ProcessNormalsFromListOfVector2(
-    const AZStd::span<const AZ::Vector2>& inPositions,
-    AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
-    Sampler sampler) const
-{
-    AZ_PROFILE_FUNCTION(Terrain);
-
-    if (!perPositionCallback)
-    {
-        return;
-    }
-
-    AZStd::vector<AZ::Vector3> inPositionsVec3 = GenerateInputPositionsFromListOfVector2(inPositions);
-
-    ProcessNormalsFromList(inPositionsVec3, perPositionCallback, sampler);
-}
-
-void TerrainSystem::ProcessSurfaceWeightsFromListOfVector2(
-    const AZStd::span<const AZ::Vector2>& inPositions,
-    AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
-    Sampler sampler) const
-{
-    AZ_PROFILE_FUNCTION(Terrain);
-
-    if (!perPositionCallback)
-    {
-        return;
-    }
-
-    AZStd::vector<AZ::Vector3> inPositionsVec3 = GenerateInputPositionsFromListOfVector2(inPositions);
-
-    ProcessSurfaceWeightsFromList(inPositionsVec3, perPositionCallback, sampler);
-}
-
-void TerrainSystem::ProcessSurfacePointsFromListOfVector2(
+void TerrainSystem::QueryListOfVector2(
     const AZStd::span<const AZ::Vector2>& inPositions,
+    TerrainDataMask requestedData,
     AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
     Sampler sampler) const
 {
@@ -1189,26 +1091,15 @@ void TerrainSystem::ProcessSurfacePointsFromListOfVector2(
 
     AZStd::vector<AZ::Vector3> inPositionsVec3 = GenerateInputPositionsFromListOfVector2(inPositions);
 
-    ProcessSurfacePointsFromList(inPositionsVec3, perPositionCallback, sampler);
-}
-
-AZStd::pair<size_t, size_t> TerrainSystem::GetNumSamplesFromRegion(
-    const AZ::Aabb& inRegion,
-    const AZ::Vector2& stepSize,
-    [[maybe_unused]] Sampler sampler) const
-{
-    size_t countX = aznumeric_cast<size_t>(AZStd::floorf(inRegion.GetExtents().GetX() / stepSize.GetX()));
-    size_t countY = aznumeric_cast<size_t>(AZStd::floorf(inRegion.GetExtents().GetY() / stepSize.GetY()));
-
-    return AZStd::make_pair(countX, countY);
+    QueryList(inPositionsVec3, requestedData, perPositionCallback, sampler);
 }
 
 //! Given a set of async parameters, calculate the max number of jobs that we can use for the async call.
-int32_t TerrainSystem::CalculateMaxJobs(AZStd::shared_ptr<ProcessAsyncParams> params) const
+int32_t TerrainSystem::CalculateMaxJobs(AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> params) const
 {
     // Determine the maximum number of jobs available to split the work across for async calls.
     const int32_t numWorkerThreads = m_terrainJobManager->GetNumWorkerThreads();
-    const int32_t numJobsDesired = params ? params->m_desiredNumberOfJobs : ProcessAsyncParams::NumJobsDefault;
+    const int32_t numJobsDesired = params ? params->m_desiredNumberOfJobs : AzFramework::Terrain::QueryAsyncParams::NumJobsDefault;
     const int32_t numJobsMax = (numJobsDesired > 0) ? AZStd::min(numWorkerThreads, numJobsDesired) : numWorkerThreads;
 
     return numJobsMax;
@@ -1260,7 +1151,7 @@ void TerrainSystem::SubdivideRegionForJobs(
         // where even the entire Y range (i.e. yChoice == 1) isn't sufficient, we can stop checking, we won't find any more solutions. 
         if (yChoice == 0)
         {
-            return;
+            break;
         }
 
         // If this combination is better than a previous solution, save it as our new best solution.
@@ -1271,99 +1162,33 @@ void TerrainSystem::SubdivideRegionForJobs(
             subdivisionsY = yChoice;
             bestJobUsage = jobUsage;
 
-            // If we've found an optimal solution, early-out and return.
+            // If we've found an optimal solution, early-out.
             if (jobUsage == clampedMaxNumJobs)
             {
-                return;
+                break;
             }
         }
     }
-}
-
-void TerrainSystem::ProcessHeightsFromRegion(
-    const AZ::Aabb& inRegion,
-    const AZ::Vector2& stepSize,
-    AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
-    Sampler sampler) const
-{
-    AZ_PROFILE_FUNCTION(Terrain);
-
-    // Don't bother processing if we don't have a callback
-    if (!perPositionCallback)
-    {
-        return;
-    }
-
-    const auto [numSamplesX, numSamplesY] = GetNumSamplesFromRegion(inRegion, stepSize, sampler);
-
-    AZStd::vector<AZ::Vector3> inPositions = GenerateInputPositionsFromRegion(inRegion, stepSize, sampler);
 
-    if (inPositions.empty())
-    {
-        return;
-    }
-
-    AZStd::vector<bool> terrainExists(inPositions.size());
-    AZStd::vector<float> heights(inPositions.size());
-
-    GetHeightsSynchronous(inPositions, sampler, heights, terrainExists);
-    
-    AzFramework::SurfaceData::SurfacePoint surfacePoint;
-    for (size_t y = 0, i = 0; y < numSamplesY; y++)
-    {
-        for (size_t x = 0; x < numSamplesX; x++)
-        {
-            surfacePoint.m_position.Set(inPositions[i].GetX(), inPositions[i].GetY(), heights[i]);
-            perPositionCallback(x, y, surfacePoint, terrainExists[i]);
-            i++;
-        }
-    }
+    // Verify that our subdivision strategy has stayed within the max jobs constraint.
+    AZ_Assert(
+        (subdivisionsX * subdivisionsY) <= maxNumJobs, "The region was subdivided into too many jobs: %d x %d vs %d max", subdivisionsX,
+        subdivisionsY, maxNumJobs);
 }
 
-void TerrainSystem::ProcessNormalsFromRegion(
-    const AZ::Aabb& inRegion,
-    const AZ::Vector2& stepSize,
+void TerrainSystem::QueryRegion(
+    const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
+    TerrainDataMask requestedData,
     AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
     Sampler sampler) const
 {
-    AZ_PROFILE_FUNCTION(Terrain);
-
-    // Don't bother processing if we don't have a callback
-    if (!perPositionCallback)
-    {
-        return;
-    }
-
-    const auto [numSamplesX, numSamplesY] = GetNumSamplesFromRegion(inRegion, stepSize, sampler);
-
-    AZStd::vector<AZ::Vector3> inPositions = GenerateInputPositionsFromRegion(inRegion, stepSize, sampler);
-
-    if (inPositions.empty())
-    {
-        return;
-    }
-
-    AZStd::vector<bool> terrainExists(inPositions.size());
-    AZStd::vector<AZ::Vector3> normals(inPositions.size());
-
-    GetNormalsSynchronous(inPositions, sampler, normals, terrainExists);
-
-    AzFramework::SurfaceData::SurfacePoint surfacePoint;
-    for (size_t y = 0, i = 0; y < numSamplesY; y++)
-    {
-        for (size_t x = 0; x < numSamplesX; x++)
-        {
-            surfacePoint.m_position.Set(inPositions[i].GetX(), inPositions[i].GetY(), 0.0f);
-            surfacePoint.m_normal = AZStd::move(normals[i]);
-            perPositionCallback(x, y, surfacePoint, terrainExists[i]);
-            i++;
-        }
-    }
+    QueryRegionInternal(queryRegion, 0, 0, requestedData, perPositionCallback, sampler);
 }
 
-void TerrainSystem::ProcessSurfaceWeightsFromRegion(
-    const AZ::Aabb& inRegion,
-    const AZ::Vector2& stepSize,
+void TerrainSystem::QueryRegionInternal(
+    const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
+    size_t xIndexOffset, size_t yIndexOffset,
+    TerrainDataMask requestedData,
     AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
     Sampler sampler) const
 {
@@ -1375,83 +1200,64 @@ void TerrainSystem::ProcessSurfaceWeightsFromRegion(
         return;
     }
 
-    const auto [numSamplesX, numSamplesY] = GetNumSamplesFromRegion(inRegion, stepSize, sampler);
-
-    AZStd::vector<AZ::Vector3> inPositions = GenerateInputPositionsFromRegion(inRegion, stepSize, sampler);
+    AZStd::vector<AZ::Vector3> inPositions = GenerateInputPositionsFromRegion(queryRegion);
 
     if (inPositions.empty())
     {
         return;
     }
 
-    AZStd::vector<AzFramework::SurfaceData::SurfaceTagWeightList> outSurfaceWeightsList(inPositions.size());
     AZStd::vector<bool> terrainExists(inPositions.size());
+    AZStd::vector<float> heights;
+    AZStd::vector<AZ::Vector3> normals;
+    AZStd::vector<AzFramework::SurfaceData::SurfaceTagWeightList> surfaceWeights;
 
-    GetOrderedSurfaceWeightsFromList(inPositions, sampler, outSurfaceWeightsList, terrainExists);
-
-    AzFramework::SurfaceData::SurfacePoint surfacePoint;
-    for (size_t y = 0, i = 0; y < numSamplesY; y++)
+    if (requestedData & TerrainDataMask::Heights)
     {
-        for (size_t x = 0; x < numSamplesX; x++)
-        {
-            surfacePoint.m_position.Set(inPositions[i].GetX(), inPositions[i].GetY(), 0.0f);
-            surfacePoint.m_surfaceTags = AZStd::move(outSurfaceWeightsList[i]);
-            perPositionCallback(x, y, surfacePoint, terrainExists[i]);
-            i++;
-        }
+        heights.resize(inPositions.size());
+        GetHeightsSynchronous(inPositions, sampler, heights, terrainExists);
     }
-}
-
-void TerrainSystem::ProcessSurfacePointsFromRegion(
-    const AZ::Aabb& inRegion,
-    const AZ::Vector2& stepSize,
-    AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
-    Sampler sampler) const
-{
-    AZ_PROFILE_FUNCTION(Terrain);
-
-    // Don't bother processing if we don't have a callback
-    if (!perPositionCallback)
+    if (requestedData & TerrainDataMask::Normals)
     {
-        return;
+        normals.resize(inPositions.size());
+        GetNormalsSynchronous(inPositions, sampler, normals, terrainExists);
     }
-
-    const auto [numSamplesX, numSamplesY] = GetNumSamplesFromRegion(inRegion, stepSize, sampler);
-
-    AZStd::vector<AZ::Vector3> inPositions = GenerateInputPositionsFromRegion(inRegion, stepSize, sampler);
-
-    if (inPositions.empty())
+    if (requestedData & TerrainDataMask::SurfaceData)
     {
-        return;
-    }
-
-    AZStd::vector<float> heights(inPositions.size());
-    AZStd::vector<AZ::Vector3> normals(inPositions.size());
-    AZStd::vector<AzFramework::SurfaceData::SurfaceTagWeightList> outSurfaceWeightsList(inPositions.size());
-    AZStd::vector<bool> terrainExists(inPositions.size());
-
-    GetHeightsSynchronous(inPositions, sampler, heights, terrainExists);
-    GetNormalsSynchronous(inPositions, sampler, normals, terrainExists);
-
-    // We can skip the unnecessary call to GetHeights since we already
-    // got the terrain exists flags in the earlier call to GetHeights
-    AZStd::vector<bool> terrainExistsEmpty;
-    GetOrderedSurfaceWeightsFromList(inPositions, sampler, outSurfaceWeightsList, terrainExistsEmpty);
+        // We can potentially skip an extra call to GetHeights if we already
+        // got the terrain exists flags in the earlier call to GetHeights
+        AZStd::vector<bool> terrainExistsEmpty;
 
+        surfaceWeights.resize(inPositions.size());
+        GetOrderedSurfaceWeightsFromList(
+            inPositions, sampler, surfaceWeights, (requestedData & TerrainDataMask::Heights) ? terrainExistsEmpty : terrainExists);
+    }
+    
     AzFramework::SurfaceData::SurfacePoint surfacePoint;
-    for (size_t y = 0, i = 0; y < numSamplesY; y++)
+    for (size_t y = 0, i = 0; y < queryRegion.m_numPointsY; y++)
     {
-        for (size_t x = 0; x < numSamplesX; x++)
+        for (size_t x = 0; x < queryRegion.m_numPointsX; x++)
         {
-            surfacePoint.m_position.Set(inPositions[i].GetX(), inPositions[i].GetY(), heights[i]);
-            surfacePoint.m_normal = AZStd::move(normals[i]);
-            surfacePoint.m_surfaceTags = AZStd::move(outSurfaceWeightsList[i]);
-            perPositionCallback(x, y, surfacePoint, terrainExists[i]);
+            surfacePoint.m_position = inPositions[i];
+            if (requestedData & TerrainDataMask::Heights)
+            {
+                surfacePoint.m_position.SetZ(heights[i]);
+            }
+            if (requestedData & TerrainDataMask::Normals)
+            {
+                surfacePoint.m_normal = AZStd::move(normals[i]);
+            }
+            if (requestedData & TerrainDataMask::SurfaceData)
+            {
+                surfacePoint.m_surfaceTags = AZStd::move(surfaceWeights[i]);
+            }
+            perPositionCallback(x + xIndexOffset, y + yIndexOffset, surfacePoint, terrainExists[i]);
             i++;
         }
     }
 }
 
+
 void TerrainSystem::RegisterArea(AZ::EntityId areaId)
 {
     AZStd::unique_lock<AZStd::shared_mutex> lock(m_areaMutex);

+ 53 - 249
Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h

@@ -144,52 +144,22 @@ namespace Terrain
 
         //! Given a list of XY coordinates, call the provided callback function with surface data corresponding to each
         //! XY coordinate in the list.
-        void ProcessHeightsFromList(const AZStd::span<const AZ::Vector3>& inPositions,
-            AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
-            Sampler sampler = Sampler::DEFAULT) const override;
-        void ProcessNormalsFromList(const AZStd::span<const AZ::Vector3>& inPositions,
-            AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
-            Sampler sampler = Sampler::DEFAULT) const override;
-        void ProcessSurfaceWeightsFromList(const AZStd::span<const AZ::Vector3>& inPositions,
-            AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
-            Sampler sampler = Sampler::DEFAULT) const override;
-        void ProcessSurfacePointsFromList(const AZStd::span<const AZ::Vector3>& inPositions,
-            AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
-            Sampler sampler = Sampler::DEFAULT) const override;
-        void ProcessHeightsFromListOfVector2(const AZStd::span<const AZ::Vector2>& inPositions,
-            AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
-            Sampler sampler = Sampler::DEFAULT) const override;
-        void ProcessNormalsFromListOfVector2(const AZStd::span<const AZ::Vector2>& inPositions,
-            AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
-            Sampler sampler = Sampler::DEFAULT) const override;
-        void ProcessSurfaceWeightsFromListOfVector2(const AZStd::span<const AZ::Vector2>& inPositions,
+        void QueryList(
+            const AZStd::span<const AZ::Vector3>& inPositions,
+            TerrainDataMask requestedData,
             AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
             Sampler sampler = Sampler::DEFAULT) const override;
-        void ProcessSurfacePointsFromListOfVector2(const AZStd::span<const AZ::Vector2>& inPositions,
+        void QueryListOfVector2(
+            const AZStd::span<const AZ::Vector2>& inPositions,
+            TerrainDataMask requestedData,
             AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
             Sampler sampler = Sampler::DEFAULT) const override;
 
-        //! Returns the number of samples for a given region and step size. The first and second
-        //! elements of the pair correspond to the X and Y sample counts respectively.
-        AZStd::pair<size_t, size_t> GetNumSamplesFromRegion(const AZ::Aabb& inRegion,
-            const AZ::Vector2& stepSize, Sampler sampler) const override;
-
         //! Given a region(aabb) and a step size, call the provided callback function with surface data corresponding to the
         //! coordinates in the region.
-        void ProcessHeightsFromRegion(const AZ::Aabb& inRegion,
-            const AZ::Vector2& stepSize,
-            AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
-            Sampler sampler = Sampler::DEFAULT) const override;
-        void ProcessNormalsFromRegion(const AZ::Aabb& inRegion,
-            const AZ::Vector2& stepSize,
-            AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
-            Sampler sampler = Sampler::DEFAULT) const override;
-        void ProcessSurfaceWeightsFromRegion(const AZ::Aabb& inRegion,
-            const AZ::Vector2& stepSize,
-            AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
-            Sampler sampler = Sampler::DEFAULT) const override;
-        void ProcessSurfacePointsFromRegion(const AZ::Aabb& inRegion,
-            const AZ::Vector2& stepSize,
+        void QueryRegion(
+            const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
+            TerrainDataMask requestedData,
             AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
             Sampler sampler = Sampler::DEFAULT) const override;
 
@@ -197,96 +167,50 @@ namespace Terrain
         AzFramework::RenderGeometry::RayResult GetClosestIntersection(
             const AzFramework::RenderGeometry::RayRequest& ray) const override;
 
-        AZStd::shared_ptr<TerrainJobContext> ProcessHeightsFromListAsync(
+        AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> QueryListAsync(
             const AZStd::span<const AZ::Vector3>& inPositions,
+            TerrainDataMask requestedData,
             AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
             Sampler sampler = Sampler::DEFAULT,
-            AZStd::shared_ptr<ProcessAsyncParams> params = nullptr) const override;
-        AZStd::shared_ptr<TerrainJobContext> ProcessNormalsFromListAsync(
-            const AZStd::span<const AZ::Vector3>& inPositions,
-            AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
-            Sampler sampler = Sampler::DEFAULT,
-            AZStd::shared_ptr<ProcessAsyncParams> params = nullptr) const override;
-        AZStd::shared_ptr<TerrainJobContext> ProcessSurfaceWeightsFromListAsync(
-            const AZStd::span<const AZ::Vector3>& inPositions,
-            AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
-            Sampler sampler = Sampler::DEFAULT,
-            AZStd::shared_ptr<ProcessAsyncParams> params = nullptr) const override;
-        AZStd::shared_ptr<TerrainJobContext> ProcessSurfacePointsFromListAsync(
-            const AZStd::span<const AZ::Vector3>& inPositions,
-            AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
-            Sampler sampler = Sampler::DEFAULT,
-            AZStd::shared_ptr<ProcessAsyncParams> params = nullptr) const override;
-        AZStd::shared_ptr<TerrainJobContext> ProcessHeightsFromListOfVector2Async(
-            const AZStd::span<const AZ::Vector2>& inPositions,
-            AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
-            Sampler sampler = Sampler::DEFAULT,
-            AZStd::shared_ptr<ProcessAsyncParams> params = nullptr) const override;
-        AZStd::shared_ptr<TerrainJobContext> ProcessNormalsFromListOfVector2Async(
-            const AZStd::span<const AZ::Vector2>& inPositions,
-            AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
-            Sampler sampler = Sampler::DEFAULT,
-            AZStd::shared_ptr<ProcessAsyncParams> params = nullptr) const override;
-        AZStd::shared_ptr<TerrainJobContext> ProcessSurfaceWeightsFromListOfVector2Async(
-            const AZStd::span<const AZ::Vector2>& inPositions,
-            AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
-            Sampler sampler = Sampler::DEFAULT,
-            AZStd::shared_ptr<ProcessAsyncParams> params = nullptr) const override;
-        AZStd::shared_ptr<TerrainJobContext> ProcessSurfacePointsFromListOfVector2Async(
+            AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> params = nullptr) const override;
+        AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> QueryListOfVector2Async(
             const AZStd::span<const AZ::Vector2>& inPositions,
+            TerrainDataMask requestedData,
             AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
             Sampler sampler = Sampler::DEFAULT,
-            AZStd::shared_ptr<ProcessAsyncParams> params = nullptr) const override;
-        AZStd::shared_ptr<TerrainJobContext> ProcessHeightsFromRegionAsync(
-            const AZ::Aabb& inRegion,
-            const AZ::Vector2& stepSize,
-            AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
-            Sampler sampler = Sampler::DEFAULT,
-            AZStd::shared_ptr<ProcessAsyncParams> params = nullptr) const override;
-        AZStd::shared_ptr<TerrainJobContext> ProcessNormalsFromRegionAsync(
-            const AZ::Aabb& inRegion,
-            const AZ::Vector2& stepSize,
-            AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
-            Sampler sampler = Sampler::DEFAULT,
-            AZStd::shared_ptr<ProcessAsyncParams> params = nullptr) const override;
-        AZStd::shared_ptr<TerrainJobContext> ProcessSurfaceWeightsFromRegionAsync(
-            const AZ::Aabb& inRegion,
-            const AZ::Vector2& stepSize,
-            AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
-            Sampler sampler = Sampler::DEFAULT,
-            AZStd::shared_ptr<ProcessAsyncParams> params = nullptr) const override;
-        AZStd::shared_ptr<TerrainJobContext> ProcessSurfacePointsFromRegionAsync(
-            const AZ::Aabb& inRegion,
-            const AZ::Vector2& stepSize,
+            AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> params = nullptr) const override;
+        AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> QueryRegionAsync(
+            const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
+            TerrainDataMask requestedData,
             AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
             Sampler sampler = Sampler::DEFAULT,
-            AZStd::shared_ptr<ProcessAsyncParams> params = nullptr) const override;
+            AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> params = nullptr) const override;
 
     private:
         //! Given a set of async parameters, calculate the max number of jobs that we can use for the async call.
-        int32_t CalculateMaxJobs(AZStd::shared_ptr<ProcessAsyncParams> params) const;
+        int32_t CalculateMaxJobs(AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> params) const;
 
         //! Given the number of samples in a region and the desired number of jobs, choose the best subdivision of the region into jobs.
         static void SubdivideRegionForJobs(
             int32_t numSamplesX, int32_t numSamplesY, int32_t maxNumJobs, int32_t minPointsPerJob,
             int32_t& subdivisionsX, int32_t& subdivisionsY);
 
-        template<typename SynchronousFunctionType, typename VectorType>
-        AZStd::shared_ptr<TerrainJobContext> ProcessFromListAsync(
-            SynchronousFunctionType synchronousFunction,
+        //! This performs the logic for QueryRegion, but also accepts x and y index offsets so that the subregions for QueryRegionAsync
+        //! can pass the correct x and y indices down to the subregion perPositionCallbacks.
+        void QueryRegionInternal(
+            const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
+            size_t xIndexOffset, size_t yIndexOffset,
+            TerrainDataMask requestedData,
+            AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
+            Sampler sampler) const;
+
+        template<typename VectorType>
+        AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> ProcessFromListAsync(
             const AZStd::span<const VectorType>& inPositions,
+            TerrainDataMask requestedData,
             AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
             Sampler sampler = Sampler::DEFAULT,
-            AZStd::shared_ptr<ProcessAsyncParams> params = nullptr) const;
-
-        template<typename SynchronousFunctionType>
-        AZStd::shared_ptr<TerrainJobContext> ProcessFromRegionAsync(
-            SynchronousFunctionType synchronousFunction,
-            const AZ::Aabb& inRegion,
-            const AZ::Vector2& stepSize,
-            AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
-            Sampler sampler = Sampler::DEFAULT,
-            AZStd::shared_ptr<ProcessAsyncParams> params = nullptr) const;
+            AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> params = nullptr) const;
 
         void ClampPosition(float x, float y, AZ::Vector2& outPosition, AZ::Vector2& normalizedDelta) const;
         bool InWorldBounds(float x, float y) const;
@@ -331,9 +255,7 @@ namespace Terrain
             AZStd::vector<AZ::Vector3>& outPositions,
             Sampler sampler) const;
         AZStd::vector<AZ::Vector3> GenerateInputPositionsFromRegion(
-            const AZ::Aabb& inRegion,
-            const AZ::Vector2& stepSize,
-            Sampler sampler) const;
+            const AzFramework::Terrain::TerrainQueryRegion& queryRegion) const;
         AZStd::vector<AZ::Vector3> GenerateInputPositionsFromListOfVector2(
             const AZStd::span<const AZ::Vector2> inPositionsVec2) const;
 
@@ -371,16 +293,16 @@ namespace Terrain
         AZ::JobManager* m_terrainJobManager = nullptr;
         mutable AZStd::mutex m_activeTerrainJobContextMutex;
         mutable AZStd::condition_variable m_activeTerrainJobContextMutexConditionVariable;
-        mutable AZStd::deque<AZStd::shared_ptr<TerrainJobContext>> m_activeTerrainJobContexts;
+        mutable AZStd::deque<AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext>> m_activeTerrainJobContexts;
     };
 
-    template<typename SynchronousFunctionType, typename VectorType>
-    inline AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext> TerrainSystem::ProcessFromListAsync(
-        SynchronousFunctionType synchronousFunction,
+    template<typename VectorType>
+    inline AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> TerrainSystem::ProcessFromListAsync(
         const AZStd::span<const VectorType>& inPositions,
+        TerrainDataMask requestedData,
         AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
         Sampler sampler,
-        AZStd::shared_ptr<ProcessAsyncParams> params) const
+        AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> params) const
     {
         AZ_PROFILE_FUNCTION(Terrain);
 
@@ -394,8 +316,9 @@ namespace Terrain
 
         // Determine the maximum number of jobs, and the minimum number of positions that should be processed per job.
         const int32_t numJobsMax = CalculateMaxJobs(params);
-        const int32_t minPositionsPerJob =
-            params && (params->m_minPositionsPerJob > 0) ? params->m_minPositionsPerJob : ProcessAsyncParams::MinPositionsPerJobDefault;
+        const int32_t minPositionsPerJob = params && (params->m_minPositionsPerJob > 0)
+            ? params->m_minPositionsPerJob
+            : AzFramework::Terrain::QueryAsyncParams::MinPositionsPerJobDefault;
 
         // Based on the above, we'll create the maximum number of jobs possible that meet both criteria:
         // - processes at least minPositionsPerJob for each job
@@ -403,7 +326,8 @@ namespace Terrain
         const int32_t numJobs = AZStd::clamp(numPositionsToProcess / minPositionsPerJob, 1, numJobsMax);
 
         // Create a terrain job context, track it, and split the work across multiple jobs.
-        AZStd::shared_ptr<TerrainJobContext> jobContext = AZStd::make_shared<TerrainJobContext>(*m_terrainJobManager, numJobs);
+        AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> jobContext =
+            AZStd::make_shared<AzFramework::Terrain::TerrainJobContext>(*m_terrainJobManager, numJobs);
         {
             AZStd::unique_lock<AZStd::mutex> lock(m_activeTerrainJobContextMutex);
             m_activeTerrainJobContexts.push_back(jobContext);
@@ -418,12 +342,19 @@ namespace Terrain
 
             // Define the job function using the sub span of positions to process.
             const AZStd::span<const VectorType>& positionsToProcess = inPositions.subspan(subSpanOffset, subSpanCount);
-            auto jobFunction = [this, synchronousFunction, positionsToProcess, perPositionCallback, sampler, jobContext, params]()
+            auto jobFunction = [this, positionsToProcess, requestedData, perPositionCallback, sampler, jobContext, params]()
             {
                 // Process the sub span of positions, unless the associated job context has been cancelled.
                 if (!jobContext->IsCancelled())
                 {
-                    synchronousFunction(positionsToProcess, perPositionCallback, sampler);
+                    if constexpr (AZStd::is_same<VectorType, AZ::Vector3>::value)
+                    {
+                        QueryList(positionsToProcess, requestedData, perPositionCallback, sampler);
+                    }
+                    else
+                    {
+                        QueryListOfVector2(positionsToProcess, requestedData, perPositionCallback, sampler);
+                    }
                 }
 
                 // Decrement the number of completions remaining, invoke the completion callback if this happens
@@ -454,131 +385,4 @@ namespace Terrain
         return jobContext;
     }
 
-    template<typename SynchronousFunctionType>
-    inline AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext> TerrainSystem::ProcessFromRegionAsync(
-        SynchronousFunctionType synchronousFunction,
-        const AZ::Aabb& inRegion,
-        const AZ::Vector2& stepSize,
-        AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
-        Sampler sampler,
-        AZStd::shared_ptr<ProcessAsyncParams> params) const
-    {
-        AZ_PROFILE_FUNCTION(Terrain);
-
-        const auto [numSamplesX, numSamplesY] = GetNumSamplesFromRegion(inRegion, stepSize, sampler);
-        const int64_t numPositionsToProcess = numSamplesX * numSamplesY;
-
-        if (numPositionsToProcess == 0)
-        {
-            AZ_Warning("TerrainSystem", false, "No positions to process.");
-            return nullptr;
-        }
-
-        // Determine the maximum number of jobs, and the minimum number of positions that should be processed per job.
-        const int32_t numJobsMax = CalculateMaxJobs(params);
-        const int32_t minPositionsPerJob =
-            params && (params->m_minPositionsPerJob > 0) ? params->m_minPositionsPerJob : ProcessAsyncParams::MinPositionsPerJobDefault;
-
-        // Calculate the best subdivision of the region along both the X and Y axes to use as close to the maximum number of jobs
-        // as possible while also keeping all the regions effectively the same size.
-        int32_t xJobs, yJobs;
-        SubdivideRegionForJobs(
-            aznumeric_cast<int32_t>(numSamplesX), aznumeric_cast<int32_t>(numSamplesY), numJobsMax, minPositionsPerJob, xJobs, yJobs);
-
-        // The number of jobs returned might be less than the total requested maximum number of jobs, so recalculate it here
-        AZ_Assert((xJobs * yJobs) <= numJobsMax, "The region was subdivided into too many jobs: %d x %d vs %d max",
-            xJobs, yJobs, numJobsMax);
-        const int32_t numJobs = xJobs * yJobs;
-
-        // Get the number of samples in each direction that we'll use for each query. We calculate this as a fractional value
-        // so that we can keep each query pretty evenly balanced, with just +/- 1 count variation on each axis.
-        const float xSamplesPerQuery = aznumeric_cast<float>(numSamplesX) / xJobs;
-        const float ySamplesPerQuery = aznumeric_cast<float>(numSamplesY) / yJobs;
-
-        // Make sure our subdivisions are producing at least minPositionsPerJob unless the *total* requested point count is
-        // less than minPositionsPerJob.
-        AZ_Assert(
-            ((numSamplesX * numSamplesY) < minPositionsPerJob) ||
-                (aznumeric_cast<int32_t>(xSamplesPerQuery) * aznumeric_cast<int32_t>(ySamplesPerQuery)) >= minPositionsPerJob,
-            "Too few positions per job: %d vs %d", aznumeric_cast<int32_t>(xSamplesPerQuery) * aznumeric_cast<int32_t>(ySamplesPerQuery),
-            minPositionsPerJob);
-
-        // Create a terrain job context and split the work across multiple jobs.
-        AZStd::shared_ptr<TerrainJobContext> jobContext = AZStd::make_shared<TerrainJobContext>(*m_terrainJobManager, numJobs);
-        {
-            AZStd::unique_lock<AZStd::mutex> lock(m_activeTerrainJobContextMutex);
-            m_activeTerrainJobContexts.push_back(jobContext);
-        }
-
-        [[maybe_unused]] int32_t jobsStarted = 0;
-
-        for (int32_t yJob = 0; yJob < yJobs; yJob++)
-        {
-            // Use the fractional samples per query to calculate the start and end of the region, but then convert it
-            // back to integers so that our regions are always in exact multiples of the number of samples to process.
-            // This is important because we want the XY values for each point that we're processing to exactly align with
-            // 'start + N * (step size)', or else we'll start to process point locations that weren't actually what was requested.
-            int32_t y0 = aznumeric_cast<int32_t>(yJob * ySamplesPerQuery);
-            int32_t y1 = aznumeric_cast<int32_t>((yJob + 1) * ySamplesPerQuery);
-            const float inRegionMinY = inRegion.GetMin().GetY() + (y0 * stepSize.GetY());
-
-            // For the last iteration, just set the end to the max to ensure that floating-point drift doesn't cause us to
-            // misalign and miss a point.
-            const float inRegionMaxY =
-                (yJob == (yJobs - 1)) ? inRegion.GetMax().GetY() : inRegion.GetMin().GetY() + (y1 * stepSize.GetY());
-
-            for (int32_t xJob = 0; xJob < xJobs; xJob++)
-            {
-                // Same as above, calculate the start and end of the region, then convert back to integers and create the
-                // region based on 'start + n * (step size)'.
-                int32_t x0 = aznumeric_cast<int32_t>(xJob * xSamplesPerQuery);
-                int32_t x1 = aznumeric_cast<int32_t>((xJob + 1) * xSamplesPerQuery);
-                const float inRegionMinX = inRegion.GetMin().GetX() + (x0 * stepSize.GetX());
-                const float inRegionMaxX =
-                    (xJob == (xJobs - 1)) ? inRegion.GetMax().GetX() : inRegion.GetMin().GetX() + (x1 * stepSize.GetX());
-
-                // Define the job function using the sub region of positions to process.
-                AZ::Aabb subRegion = AZ::Aabb::CreateFromMinMax(
-                    AZ::Vector3(inRegionMinX, inRegionMinY, inRegion.GetMin().GetZ()),
-                    AZ::Vector3(inRegionMaxX, inRegionMaxY, inRegion.GetMax().GetZ()));
-
-                auto jobFunction = [this, synchronousFunction, subRegion, stepSize, perPositionCallback, sampler, jobContext, params]()
-                {
-                    // Process the sub region of positions, unless the associated job context has been cancelled.
-                    if (!jobContext->IsCancelled())
-                    {
-                        synchronousFunction(subRegion, stepSize, perPositionCallback, sampler);
-                    }
-
-                    // Decrement the number of completions remaining, invoke the completion callback if this happens
-                    // to be the final job completed, and remove this TerrainJobContext from the list of active ones.
-                    const bool wasLastJobCompleted = jobContext->OnJobCompleted();
-                    if (wasLastJobCompleted)
-                    {
-                        if (params && params->m_completionCallback)
-                        {
-                            params->m_completionCallback(jobContext);
-                        }
-
-                        {
-                            AZStd::unique_lock<AZStd::mutex> lock(m_activeTerrainJobContextMutex);
-                            m_activeTerrainJobContexts.erase(
-                                AZStd::find(m_activeTerrainJobContexts.begin(), m_activeTerrainJobContexts.end(), jobContext));
-                            m_activeTerrainJobContextMutexConditionVariable.notify_one();
-                        }
-                    }
-                };
-
-                // Create the job and start it immediately.
-                AZ::Job* processJob = AZ::CreateJobFunction(jobFunction, true, jobContext.get());
-                processJob->Start();
-                jobsStarted++;
-            }
-        }
-
-        // Validate this just to ensure that the fractional math for handling points didn't cause any rounding errors anywhere.
-        AZ_Assert(jobsStarted == numJobs, "Wrong number of jobs created: %d vs %d", jobsStarted, numJobs);
-
-        return jobContext;
-    }
 } // namespace Terrain

+ 59 - 37
Gems/Terrain/Code/Tests/TerrainBulkQueryTests.cpp

@@ -48,9 +48,11 @@ namespace UnitTest::TerrainTest
                 EXPECT_TRUE(terrainExists);
             };
 
+            AzFramework::Terrain::TerrainQueryRegion queryRegion =
+                AzFramework::Terrain::TerrainQueryRegion::CreateFromAabbAndStepSize(inputQueryRegion, inputQueryStepSize);
             AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                &AzFramework::Terrain::TerrainDataRequests::ProcessHeightsFromRegion, inputQueryRegion, inputQueryStepSize,
-                perPositionCallback, sampler);
+                &AzFramework::Terrain::TerrainDataRequests::QueryRegion, queryRegion,
+                AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Heights, perPositionCallback, sampler);
 
             EXPECT_EQ(queryPositions.size(), resultPositions.size());
         }
@@ -99,9 +101,11 @@ namespace UnitTest::TerrainTest
                 EXPECT_TRUE(terrainExists);
             };
 
+            AzFramework::Terrain::TerrainQueryRegion queryRegion =
+                AzFramework::Terrain::TerrainQueryRegion::CreateFromAabbAndStepSize(inputQueryRegion, inputQueryStepSize);
             AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                &AzFramework::Terrain::TerrainDataRequests::ProcessNormalsFromRegion, inputQueryRegion, inputQueryStepSize,
-                perPositionCallback, sampler);
+                &AzFramework::Terrain::TerrainDataRequests::QueryRegion, queryRegion,
+                AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Normals, perPositionCallback, sampler);
 
             EXPECT_EQ(queryPositions.size(), resultNormals.size());
         }
@@ -157,9 +161,11 @@ namespace UnitTest::TerrainTest
                 EXPECT_TRUE(terrainExists);
             };
 
+            AzFramework::Terrain::TerrainQueryRegion queryRegion =
+                AzFramework::Terrain::TerrainQueryRegion::CreateFromAabbAndStepSize(inputQueryRegion, inputQueryStepSize);
             AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                &AzFramework::Terrain::TerrainDataRequests::ProcessSurfaceWeightsFromRegion, inputQueryRegion, inputQueryStepSize,
-                perPositionCallback, sampler);
+                &AzFramework::Terrain::TerrainDataRequests::QueryRegion, queryRegion,
+                AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::SurfaceData, perPositionCallback, sampler);
 
             EXPECT_EQ(queryPositions.size(), resultWeights.size());
         }
@@ -215,9 +221,11 @@ namespace UnitTest::TerrainTest
                 EXPECT_TRUE(terrainExists);
             };
 
+            AzFramework::Terrain::TerrainQueryRegion queryRegion =
+                AzFramework::Terrain::TerrainQueryRegion::CreateFromAabbAndStepSize(inputQueryRegion, inputQueryStepSize);
             AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                &AzFramework::Terrain::TerrainDataRequests::ProcessSurfacePointsFromRegion, inputQueryRegion, inputQueryStepSize,
-                perPositionCallback, sampler);
+                &AzFramework::Terrain::TerrainDataRequests::QueryRegion, queryRegion,
+                AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::All, perPositionCallback, sampler);
 
             EXPECT_EQ(queryPositions.size(), resultPoints.size());
         }
@@ -252,14 +260,14 @@ namespace UnitTest::TerrainTest
             }
         }
 
-        AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams> CreateTestAsyncParams()
+        AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> CreateTestAsyncParams()
         {
-            auto params = AZStd::make_shared<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams>();
+            auto params = AZStd::make_shared<AzFramework::Terrain::QueryAsyncParams>();
 
             // Set the number of jobs > 1 so that we have parallel queries that execute.
             params->m_desiredNumberOfJobs = 4;
             params->m_completionCallback =
-                [this]([[maybe_unused]] AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext> context)
+                [this]([[maybe_unused]] AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> context)
             {
                 // Notify the main test thread that the query has completed.
                 m_queryCompletionEvent.release();
@@ -315,7 +323,8 @@ namespace UnitTest::TerrainTest
             };
 
             AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                &AzFramework::Terrain::TerrainDataRequests::ProcessHeightsFromList, queryPositions, listPositionCallback, sampler);
+                &AzFramework::Terrain::TerrainDataRequests::QueryList,
+                queryPositions, AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Heights, listPositionCallback, sampler);
 
             // Compare the results
             ComparePositionData(baselineResultPositions, comparisonResultPositions);
@@ -388,10 +397,12 @@ namespace UnitTest::TerrainTest
             };
 
             auto params = CreateTestAsyncParams();
-            AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext> jobContext;
+            AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> jobContext;
+            AzFramework::Terrain::TerrainQueryRegion queryRegion =
+                AzFramework::Terrain::TerrainQueryRegion::CreateFromAabbAndStepSize(QueryBounds, QueryStepSize);
             AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
-                jobContext, &AzFramework::Terrain::TerrainDataRequests::ProcessHeightsFromRegionAsync, QueryBounds, QueryStepSize,
-                regionPositionCallback, sampler, params);
+                jobContext, &AzFramework::Terrain::TerrainDataRequests::QueryRegionAsync, queryRegion,
+                AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Heights, regionPositionCallback, sampler, params);
 
             // Wait for the async query to complete
             m_queryCompletionEvent.acquire();
@@ -431,9 +442,10 @@ namespace UnitTest::TerrainTest
             };
 
             auto params = CreateTestAsyncParams();
-            AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext> jobContext;
+            AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> jobContext;
             AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
-                jobContext, &AzFramework::Terrain::TerrainDataRequests::ProcessHeightsFromListAsync, queryPositions,
+                jobContext, &AzFramework::Terrain::TerrainDataRequests::QueryListAsync, queryPositions,
+                AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Heights,
                 listPositionCallback, sampler, params);
 
             // Wait for the async query to complete
@@ -476,7 +488,8 @@ namespace UnitTest::TerrainTest
             };
 
             AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                &AzFramework::Terrain::TerrainDataRequests::ProcessNormalsFromList, queryPositions, listNormalCallback, sampler);
+                &AzFramework::Terrain::TerrainDataRequests::QueryList, queryPositions,
+                AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Normals, listNormalCallback, sampler);
 
             // Compare the results
             CompareNormalData(queryPositions, baselineResultNormals, comparisonResultPositions, comparisonResultNormals);
@@ -550,10 +563,12 @@ namespace UnitTest::TerrainTest
             };
 
             auto params = CreateTestAsyncParams();
-            AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext> jobContext;
+            AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> jobContext;
+            AzFramework::Terrain::TerrainQueryRegion queryRegion =
+                AzFramework::Terrain::TerrainQueryRegion::CreateFromAabbAndStepSize(QueryBounds, QueryStepSize);
             AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
-                jobContext, &AzFramework::Terrain::TerrainDataRequests::ProcessNormalsFromRegionAsync, QueryBounds, QueryStepSize,
-                regionPositionCallback, sampler, params);
+                jobContext, &AzFramework::Terrain::TerrainDataRequests::QueryRegionAsync, queryRegion,
+                AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Normals, regionPositionCallback, sampler, params);
 
             // Wait for the async query to complete
             m_queryCompletionEvent.acquire();
@@ -595,9 +610,10 @@ namespace UnitTest::TerrainTest
             };
 
             auto params = CreateTestAsyncParams();
-            AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext> jobContext;
+            AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> jobContext;
             AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
-                jobContext, &AzFramework::Terrain::TerrainDataRequests::ProcessNormalsFromListAsync, queryPositions, listPositionCallback,
+                jobContext, &AzFramework::Terrain::TerrainDataRequests::QueryListAsync, queryPositions,
+                AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Normals, listPositionCallback,
                 sampler, params);
 
             // Wait for the async query to complete
@@ -640,7 +656,8 @@ namespace UnitTest::TerrainTest
             };
 
             AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                &AzFramework::Terrain::TerrainDataRequests::ProcessSurfaceWeightsFromList, queryPositions, listWeightsCallback, sampler);
+                &AzFramework::Terrain::TerrainDataRequests::QueryList, queryPositions,
+                AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::SurfaceData, listWeightsCallback, sampler);
 
             // Compare the results
             CompareSurfaceWeightData(queryPositions, baselineResultWeights, comparisonResultPositions, comparisonResultWeights);
@@ -714,10 +731,12 @@ namespace UnitTest::TerrainTest
             };
 
             auto params = CreateTestAsyncParams();
-            AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext> jobContext;
+            AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> jobContext;
+            AzFramework::Terrain::TerrainQueryRegion queryRegion =
+                AzFramework::Terrain::TerrainQueryRegion::CreateFromAabbAndStepSize(QueryBounds, QueryStepSize);
             AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
-                jobContext, &AzFramework::Terrain::TerrainDataRequests::ProcessSurfaceWeightsFromRegionAsync, QueryBounds, QueryStepSize,
-                perPositionCallback, sampler, params);
+                jobContext, &AzFramework::Terrain::TerrainDataRequests::QueryRegionAsync, queryRegion,
+                AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::SurfaceData, perPositionCallback, sampler, params);
 
             // Wait for the async query to complete
             m_queryCompletionEvent.acquire();
@@ -759,10 +778,10 @@ namespace UnitTest::TerrainTest
             };
 
             auto params = CreateTestAsyncParams();
-            AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext> jobContext;
+            AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> jobContext;
             AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
-                jobContext, &AzFramework::Terrain::TerrainDataRequests::ProcessSurfaceWeightsFromListAsync,
-                queryPositions, listPositionCallback, sampler, params);
+                jobContext, &AzFramework::Terrain::TerrainDataRequests::QueryListAsync, queryPositions,
+                AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::SurfaceData, listPositionCallback, sampler, params);
 
             // Wait for the async query to complete
             m_queryCompletionEvent.acquire();
@@ -802,7 +821,8 @@ namespace UnitTest::TerrainTest
             };
 
             AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                &AzFramework::Terrain::TerrainDataRequests::ProcessSurfacePointsFromList, queryPositions, listPositionCallback, sampler);
+                &AzFramework::Terrain::TerrainDataRequests::QueryList, queryPositions,
+                AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::All, listPositionCallback, sampler);
 
             // Compare the results
             CompareSurfacePointData(baselineResultPoints, comparisonResultPoints);
@@ -874,10 +894,12 @@ namespace UnitTest::TerrainTest
             };
 
             auto params = CreateTestAsyncParams();
-            AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext> jobContext;
+            AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> jobContext;
+            AzFramework::Terrain::TerrainQueryRegion queryRegion =
+                AzFramework::Terrain::TerrainQueryRegion::CreateFromAabbAndStepSize(QueryBounds, QueryStepSize);
             AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
-                jobContext, &AzFramework::Terrain::TerrainDataRequests::ProcessSurfacePointsFromRegionAsync, QueryBounds, QueryStepSize,
-                regionPositionCallback, sampler, params);
+                jobContext, &AzFramework::Terrain::TerrainDataRequests::QueryRegionAsync, queryRegion,
+                AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::All, regionPositionCallback, sampler, params);
 
             // Wait for the async query to complete
             m_queryCompletionEvent.acquire();
@@ -917,10 +939,10 @@ namespace UnitTest::TerrainTest
             };
 
             auto params = CreateTestAsyncParams();
-            AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext> jobContext;
+            AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> jobContext;
             AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
-                jobContext, &AzFramework::Terrain::TerrainDataRequests::ProcessSurfacePointsFromListAsync,
-                queryPositions, listPositionCallback, sampler, params);
+                jobContext, &AzFramework::Terrain::TerrainDataRequests::QueryListAsync, queryPositions,
+                AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::All, listPositionCallback, sampler, params);
 
             // Wait for the async query to complete
             m_queryCompletionEvent.acquire();

+ 115 - 70
Gems/Terrain/Code/Tests/TerrainPhysicsColliderTests.cpp

@@ -55,7 +55,7 @@ protected:
         m_colliderComponent = m_entity->CreateComponent<Terrain::TerrainPhysicsColliderComponent>(configuration);
     }
 
-    void ProcessRegionLoop(const AZ::Aabb& inRegion, const AZ::Vector2& stepSize,
+    void ProcessRegionLoop(AzFramework::Terrain::TerrainQueryRegion queryRegion,
         AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
         AzFramework::SurfaceData::SurfaceTagWeightList* surfaceTags,
         const AZStd::function<float(float, float)>& heightGenerator)
@@ -65,17 +65,14 @@ protected:
             return;
         }
 
-        const size_t numSamplesX = aznumeric_cast<size_t>(ceil(inRegion.GetExtents().GetX() / stepSize.GetX()));
-        const size_t numSamplesY = aznumeric_cast<size_t>(ceil(inRegion.GetExtents().GetY() / stepSize.GetY()));
-
         AzFramework::SurfaceData::SurfacePoint surfacePoint;
-        for (size_t y = 0; y < numSamplesY; y++)
+        for (size_t y = 0; y < queryRegion.m_numPointsY; y++)
         {
-            float fy = aznumeric_cast<float>(inRegion.GetMin().GetY() + (y * stepSize.GetY()));
-            for (size_t x = 0; x < numSamplesX; x++)
+            float fy = aznumeric_cast<float>(queryRegion.m_startPoint.GetY() + (y * queryRegion.m_stepSize.GetY()));
+            for (size_t x = 0; x < queryRegion.m_numPointsX; x++)
             {
                 bool terrainExists = false;
-                float fx = aznumeric_cast<float>(inRegion.GetMin().GetX() + (x * stepSize.GetX()));
+                float fx = aznumeric_cast<float>(queryRegion.m_startPoint.GetX() + (x * queryRegion.m_stepSize.GetX()));
                 surfacePoint.m_position.Set(fx, fy, heightGenerator(fx, fy));
                 if (surfaceTags)
                 {
@@ -155,15 +152,20 @@ TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderReturnsAligned
     Physics::HeightfieldProviderRequestsBus::Event(
         m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows);
 
-    // With the bounds set at 0-1024 and a resolution of 1.0, the heightfield grid should be 1024x1024.
-    EXPECT_EQ(cols, 1024);
-    EXPECT_EQ(rows, 1024);
+    // "max - min" gives us the number of grid squares, "max - min + 1" gives us the number of grid vertices including the final endcap.
+    const int32_t expectedGridSize = aznumeric_cast<int32_t>(boundsMax - boundsMin) + 1;
+
+    // With the bounds set at 0-1024 and a resolution of 1.0, the heightfield grid should be 1025x1025, because it
+    // should have a final set of vertices to end the grid.
+    // ex: bounds set from 0-2 would generate *--*--* , which is 3 points, but 2 grid boxes.
+    EXPECT_EQ(cols, expectedGridSize);
+    EXPECT_EQ(rows, expectedGridSize);
 }
 
-TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderExpandsMinBoundsCorrectly)
+TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderConstrictsMinBoundsCorrectly)
 {
-    // Check that the heightfield grid is correctly expanded if the minimum value of the bounds needs expanding
-    // to correctly encompass it.
+    // Check that the heightfield grid is correctly constricted if the minimum value of the bounds doesn't land directly
+    // on a terrain grid boundary line.
     AddTerrainPhysicsColliderToEntity(Terrain::TerrainPhysicsColliderConfig());
 
     const float boundsMin = 0.1f;
@@ -183,16 +185,19 @@ TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderExpandsMinBoun
     Physics::HeightfieldProviderRequestsBus::Event(
         m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows);
 
-    // If the heightfield is not expanded to ensure it encompasses the shape bounds,
-    // the values returned would be 1023.
-    EXPECT_EQ(cols, 1024);
-    EXPECT_EQ(rows, 1024);
+    // "max - min" gives us the number of grid squares, "max - min + 1" gives us the number of grid vertices including the final endcap.
+    // Note that this is also rounding down via the int truncation
+    const int32_t expectedGridSize = aznumeric_cast<int32_t>(boundsMax - boundsMin) + 1;
+
+    // If the heightfield is not constricted to stay within the shape bounds, the values returned would be 1025.
+    EXPECT_EQ(cols, expectedGridSize);
+    EXPECT_EQ(rows, expectedGridSize);
 }
 
-TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderExpandsMaxBoundsCorrectly)
+TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderConstrictsMaxBoundsCorrectly)
 {
-    // Check that the heightfield grid is correctly expanded if the maximum value of the bounds needs expanding
-    // to correctly encompass it.
+    // Check that the heightfield grid is correctly constricted if the maximum value of the bounds doesn't land directly
+    // on a terrain grid boundary line.
     AddTerrainPhysicsColliderToEntity(Terrain::TerrainPhysicsColliderConfig());
 
     const float boundsMin = 0.0f;
@@ -212,10 +217,13 @@ TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderExpandsMaxBoun
     Physics::HeightfieldProviderRequestsBus::Event(
         m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows);
 
-    // If the heightfield is not expanded to ensure it encompasses the shape bounds,
-    // the values returned would be 1023.
-    EXPECT_EQ(cols, 1024);
-    EXPECT_EQ(rows, 1024);
+    // "max - min" gives us the number of grid squares, "max - min + 1" gives us the number of grid vertices including the final endcap.
+    // Note that this is also rounding down via the int truncation
+    const int32_t expectedGridSize = aznumeric_cast<int32_t>(boundsMax - boundsMin) + 1;
+
+    // If the heightfield is not constricted to stay within the shape bounds, the values returned would be 1025.
+    EXPECT_EQ(cols, expectedGridSize);
+    EXPECT_EQ(rows, expectedGridSize);
 }
 
 TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderGetHeightsReturnsHeights)
@@ -233,12 +241,14 @@ TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderGetHeightsRetu
     float mockHeightResolution = 1.0f;
     NiceMock<UnitTest::MockTerrainDataRequests> terrainListener;
     ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution));
-    ON_CALL(terrainListener, ProcessHeightsFromRegion).WillByDefault(
-        [this](const AZ::Aabb& inRegion, const AZ::Vector2& stepSize,
+    ON_CALL(terrainListener, QueryRegion).WillByDefault(
+        [this](
+            const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
+            [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::TerrainDataMask requestedData,
             AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
             [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::Sampler sampleFilter)
         {
-            ProcessRegionLoop(inRegion, stepSize, perPositionCallback, nullptr,
+            ProcessRegionLoop(queryRegion, perPositionCallback, nullptr,
                 []([[maybe_unused]]float x, [[maybe_unused]]float y){ return 0.0f; });
         }
     );
@@ -254,8 +264,11 @@ TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderGetHeightsRetu
     Physics::HeightfieldProviderRequestsBus::EventResult(
         heights, m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeights);
 
-    EXPECT_EQ(cols, 1024);
-    EXPECT_EQ(rows, 1024);
+    // "max - min" gives us the number of grid squares, "max - min + 1" gives us the number of grid vertices including the final endcap.
+    const int32_t expectedGridSize = aznumeric_cast<int32_t>(boundsMax - boundsMin) + 1;
+
+    EXPECT_EQ(cols, expectedGridSize);
+    EXPECT_EQ(rows, expectedGridSize);
     EXPECT_EQ(heights.size(), cols * rows);
 }
 
@@ -272,23 +285,25 @@ TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderReturnsRelativ
 
     NiceMock<UnitTest::MockTerrainDataRequests> terrainListener;
     ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution));
-    ON_CALL(terrainListener, ProcessHeightsFromRegion).WillByDefault(
-        [this, mockHeight](const AZ::Aabb& inRegion, const AZ::Vector2& stepSize,
+    ON_CALL(terrainListener, QueryRegion).WillByDefault(
+        [this, mockHeight](
+            const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
+            [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::TerrainDataMask requestedData,
             AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
             [[maybe_unused]]  AzFramework::Terrain::TerrainDataRequests::Sampler sampleFilter)
         {
-            ProcessRegionLoop(inRegion, stepSize, perPositionCallback, nullptr,
+            ProcessRegionLoop(queryRegion, perPositionCallback, nullptr,
                 [mockHeight]([[maybe_unused]]float x, [[maybe_unused]]float y){ return mockHeight; });
         }
     );
 
-    ActivateEntity(m_entity.get());
-
     // Just return the bounds as setup. This is equivalent to the box being at the origin.
     NiceMock<UnitTest::MockShapeComponentRequests> boxShape(m_entity->GetId());
     const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(boundsMin), AZ::Vector3(boundsMax));
     ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds));
 
+    ActivateEntity(m_entity.get());
+
     AZStd::vector<float> heights;
 
     Physics::HeightfieldProviderRequestsBus::EventResult(heights, m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeights);
@@ -395,12 +410,14 @@ TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderGetHeightsAndM
 
     NiceMock<UnitTest::MockTerrainDataRequests> terrainListener;
     ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution));
-    ON_CALL(terrainListener, ProcessSurfacePointsFromRegion).WillByDefault(
-        [this, mockHeight, &surfaceTags](const AZ::Aabb& inRegion, const AZ::Vector2& stepSize,
+    ON_CALL(terrainListener, QueryRegion).WillByDefault(
+        [this, mockHeight, &surfaceTags](
+            const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
+            [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::TerrainDataMask requestedData,
             AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
             [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::Sampler sampleFilter)
         {
-            ProcessRegionLoop(inRegion, stepSize, perPositionCallback, &surfaceTags,
+            ProcessRegionLoop(queryRegion, perPositionCallback, &surfaceTags,
                 [mockHeight]([[maybe_unused]]float x, [[maybe_unused]]float y){ return mockHeight; });
         }
     );
@@ -412,19 +429,28 @@ TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderGetHeightsAndM
     Physics::HeightfieldProviderRequestsBus::EventResult(
         heightsAndMaterials, m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightsAndMaterials);
 
-    // We set the bounds to 256, so check that the correct number of entries are present.
-    EXPECT_EQ(heightsAndMaterials.size(), 256 * 256);
+    int32_t cols, rows;
+    Physics::HeightfieldProviderRequestsBus::Event(
+        m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows);
+
+    // "max - min" gives us the number of grid squares, "max - min + 1" gives us the number of grid vertices including the final endcap.
+    const int32_t expectedGridSize = aznumeric_cast<int32_t>(boundsMax.GetX() - boundsMin.GetX()) + 1;
+
+    // Check that the correct number of entries are present.
+    // We expect 257 x 257 because there should be an extra point in each direction to "cap off" each grid square.
+    EXPECT_EQ(cols, expectedGridSize);
+    EXPECT_EQ(rows, expectedGridSize);
+    EXPECT_EQ(heightsAndMaterials.size(), cols * rows);
 
     const float expectedHeightValue = 16384.0f;
 
-    // 
     // Check an entry from the first half of the returned list.
     EXPECT_EQ(heightsAndMaterials[0].m_materialIndex, 1);
     EXPECT_NEAR(heightsAndMaterials[0].m_height, expectedHeightValue, 0.01f);
 
     // Check an entry from the second half of the list
-    EXPECT_EQ(heightsAndMaterials[256 * 128].m_materialIndex, 2);
-    EXPECT_NEAR(heightsAndMaterials[256 * 128].m_height, expectedHeightValue, 0.01f);
+    EXPECT_EQ(heightsAndMaterials[cols * 128].m_materialIndex, 2);
+    EXPECT_NEAR(heightsAndMaterials[cols * 128].m_height, expectedHeightValue, 0.01f);
 }
 
 TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderDefaultMaterialAssignedWhenTagHasNoMapping)
@@ -464,12 +490,14 @@ TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderDefaultMateria
 
     NiceMock<UnitTest::MockTerrainDataRequests> terrainListener;
     ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution));
-    ON_CALL(terrainListener, ProcessSurfacePointsFromRegion).WillByDefault(
-        [this, mockHeight, &surfaceTags](const AZ::Aabb& inRegion, const AZ::Vector2& stepSize,
+    ON_CALL(terrainListener, QueryRegion).WillByDefault(
+        [this, mockHeight, &surfaceTags](
+            const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
+            [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::TerrainDataMask requestedData,
             AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
             [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::Sampler sampleFilter)
     {
-        ProcessRegionLoop(inRegion, stepSize, perPositionCallback, &surfaceTags,
+        ProcessRegionLoop(queryRegion, perPositionCallback, &surfaceTags,
             [mockHeight]([[maybe_unused]]float x, [[maybe_unused]]float y){ return mockHeight; });
     }
     );
@@ -494,15 +522,25 @@ TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderDefaultMateria
         Physics::HeightfieldProviderRequestsBus::EventResult(
             heightsAndMaterials, m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightsAndMaterials);
 
-        // We set the bounds to 256, so check that the correct number of entries are present.
-        EXPECT_EQ(heightsAndMaterials.size(), 256 * 256);
+        int32_t cols, rows;
+        Physics::HeightfieldProviderRequestsBus::Event(
+            m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows);
+
+    // "max - min" gives us the number of grid squares, "max - min + 1" gives us the number of grid vertices including the final endcap.
+        const int32_t expectedGridSize = aznumeric_cast<int32_t>(boundsMax.GetX() - boundsMin.GetX()) + 1;
+
+        // Check that the correct number of entries are present.
+        // We expect 257 x 257 because there should be an extra point in each direction to "cap off" each grid square.
+        EXPECT_EQ(cols, expectedGridSize);
+        EXPECT_EQ(rows, expectedGridSize);
+        EXPECT_EQ(heightsAndMaterials.size(), cols * rows);
 
         // Check an entry from the first half of the returned list.
         EXPECT_EQ(heightsAndMaterials[0].m_materialIndex, 1);
 
         // Check an entry from the second half of the list.
         // This should point to the default material (0) since we don't have a mapping for "tag2"
-        EXPECT_EQ(heightsAndMaterials[256 * 128].m_materialIndex, 0);
+        EXPECT_EQ(heightsAndMaterials[cols * 128].m_materialIndex, 0);
     }
 }
 
@@ -534,12 +572,14 @@ TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderDefaultMateria
 
     NiceMock<UnitTest::MockTerrainDataRequests> terrainListener;
     ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution));
-    ON_CALL(terrainListener, ProcessSurfacePointsFromRegion).WillByDefault(
-        [this, mockHeight, &surfaceTags](const AZ::Aabb& inRegion, const AZ::Vector2& stepSize,
+    ON_CALL(terrainListener, QueryRegion).WillByDefault(
+        [this, mockHeight, &surfaceTags](
+            const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
+            [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::TerrainDataMask requestedData,
             AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
             [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::Sampler sampleFilter)
     {
-        ProcessRegionLoop(inRegion, stepSize, perPositionCallback, &surfaceTags,
+        ProcessRegionLoop(queryRegion, perPositionCallback, &surfaceTags,
             [mockHeight]([[maybe_unused]]float x, [[maybe_unused]]float y){ return mockHeight; });
     }
     );
@@ -562,14 +602,24 @@ TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderDefaultMateria
         Physics::HeightfieldProviderRequestsBus::EventResult(
             heightsAndMaterials, m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightsAndMaterials);
 
-        // We set the bounds to 256, so check that the correct number of entries are present.
-        EXPECT_EQ(heightsAndMaterials.size(), 256 * 256);
+        int32_t cols, rows;
+        Physics::HeightfieldProviderRequestsBus::Event(
+            m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows);
+
+        // "max - min" gives us the number of grid squares, "max - min + 1" gives us the number of grid vertices including the final endcap.
+        const int32_t expectedGridSize = aznumeric_cast<int32_t>(boundsMax.GetX() - boundsMin.GetX()) + 1;
+
+        // Check that the correct number of entries are present.
+        // We expect 257 x 257 because there should be an extra point in each direction to "cap off" each grid square.
+        EXPECT_EQ(cols, expectedGridSize);
+        EXPECT_EQ(rows, expectedGridSize);
+        EXPECT_EQ(heightsAndMaterials.size(), cols * rows);
 
         // Check an entry from the first half of the returned list. Should be the default material index 0.
         EXPECT_EQ(heightsAndMaterials[0].m_materialIndex, 0);
 
         // Check an entry from the second half of the list. Should be the default material index 0.
-        EXPECT_EQ(heightsAndMaterials[256 * 128].m_materialIndex, 0);
+        EXPECT_EQ(heightsAndMaterials[cols * 128].m_materialIndex, 0);
     }
 }
 
@@ -579,6 +629,7 @@ TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderRequestSubpart
     AddTerrainPhysicsColliderToEntity(Terrain::TerrainPhysicsColliderConfig());
 
     const int32_t terrainSize = 256;
+    const int32_t expectedGridSize = terrainSize + 1;
 
     const AZ::Vector3 boundsMin = AZ::Vector3(0.0f);
     const AZ::Vector3 boundsMax = AZ::Vector3(terrainSize, terrainSize, 512.0f);
@@ -595,21 +646,15 @@ TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderRequestSubpart
 
     NiceMock<UnitTest::MockTerrainDataRequests> terrainListener;
     ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution));
-    ON_CALL(terrainListener, GetNumSamplesFromRegion)
-        .WillByDefault(
-            []([[maybe_unused]] const AZ::Aabb& inRegion, [[maybe_unused]] const AZ::Vector2& stepSize, 
-                [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::Sampler sampleFilter) -> AZStd::pair<size_t, size_t>
-            {
-                return AZStd::make_pair(10, 10);
-            });
-    ON_CALL(terrainListener, ProcessSurfacePointsFromRegion)
-        .WillByDefault(
-        [this, &surfaceTags](const AZ::Aabb& inRegion, const AZ::Vector2& stepSize,
+    ON_CALL(terrainListener, QueryRegion).WillByDefault(
+        [this, &surfaceTags](
+            const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
+            [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::TerrainDataMask requestedData,
             AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
             [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::Sampler sampleFilter)
     {
         // Assign a variety of heights across the terrain
-        ProcessRegionLoop(inRegion, stepSize, perPositionCallback, &surfaceTags,
+        ProcessRegionLoop(queryRegion, perPositionCallback, &surfaceTags,
             [](float x, float y){ return x + y; });
     }
     );
@@ -618,14 +663,14 @@ TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderRequestSubpart
 
     // Get the entire array of points
     AZStd::vector<Physics::HeightMaterialPoint> heightsMaterials = m_colliderComponent->GetHeightsAndMaterials();
-    EXPECT_EQ(heightsMaterials.size(), terrainSize * terrainSize);
+    EXPECT_EQ(heightsMaterials.size(), expectedGridSize * expectedGridSize);
 
     // Request a sub-part of the terrain and validate the points match the original data
     int32_t callCounter = 0;
     Physics::UpdateHeightfieldSampleFunction validateDataCallback = [&callCounter, &heightsMaterials](int32_t row,
         int32_t column, const Physics::HeightMaterialPoint& dataPoint)
     {
-        size_t lookUpIndex = row * terrainSize + column;
+        size_t lookUpIndex = row * expectedGridSize + column;
         EXPECT_LT(lookUpIndex, heightsMaterials.size());
         EXPECT_EQ(heightsMaterials[lookUpIndex].m_height, dataPoint.m_height);
         ++callCounter;
@@ -633,8 +678,8 @@ TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderRequestSubpart
 
     AZ::Vector3 regionMin(AZ::Vector3(10.0f));
     AZ::Vector3 regionMax(AZ::Vector3(200.0f));
-    int32_t dx = int32_t(regionMax.GetX() - regionMin.GetX());
-    int32_t dy = int32_t(regionMax.GetY() - regionMin.GetY());
+    int32_t dx = int32_t(regionMax.GetX() - regionMin.GetX()) + 1;
+    int32_t dy = int32_t(regionMax.GetY() - regionMin.GetY()) + 1;
             
     m_colliderComponent->UpdateHeightsAndMaterials(validateDataCallback, AZ::Aabb::CreateFromMinMax(regionMin, regionMax));
 

+ 100 - 63
Gems/Terrain/Code/Tests/TerrainSystemBenchmarks.cpp

@@ -180,8 +180,11 @@ namespace UnitTest
                 };
 
                 AZ::Vector2 stepSize = AZ::Vector2(queryResolution);
+                AzFramework::Terrain::TerrainQueryRegion queryRegion =
+                    AzFramework::Terrain::TerrainQueryRegion::CreateFromAabbAndStepSize(worldBounds, stepSize);
                 AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                    &AzFramework::Terrain::TerrainDataRequests::ProcessHeightsFromRegion, worldBounds, stepSize, perPositionCallback, sampler);
+                    &AzFramework::Terrain::TerrainDataRequests::QueryRegion, queryRegion,
+                    AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Heights, perPositionCallback, sampler);
             }
         );
     }
@@ -212,19 +215,22 @@ namespace UnitTest
                 };
 
                 AZStd::semaphore completionEvent;
-                auto completionCallback = [&completionEvent](AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext>)
+                auto completionCallback = [&completionEvent](AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext>)
                 {
                     completionEvent.release();
                 };
 
-                AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams> asyncParams
-                    = AZStd::make_shared<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams>();
-                asyncParams->m_desiredNumberOfJobs = AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams::NumJobsMax;
+                AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> asyncParams
+                    = AZStd::make_shared<AzFramework::Terrain::QueryAsyncParams>();
+                asyncParams->m_desiredNumberOfJobs = AzFramework::Terrain::QueryAsyncParams::NumJobsMax;
                 asyncParams->m_completionCallback = completionCallback;
 
                 AZ::Vector2 stepSize = AZ::Vector2(queryResolution);
+                AzFramework::Terrain::TerrainQueryRegion queryRegion =
+                    AzFramework::Terrain::TerrainQueryRegion::CreateFromAabbAndStepSize(worldBounds, stepSize);
                 AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                    &AzFramework::Terrain::TerrainDataRequests::ProcessHeightsFromRegionAsync, worldBounds, stepSize, perPositionCallback, sampler, asyncParams);
+                    &AzFramework::Terrain::TerrainDataRequests::QueryRegionAsync, queryRegion,
+                    AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Heights, perPositionCallback, sampler, asyncParams);
 
                 completionEvent.acquire();
             }
@@ -259,7 +265,8 @@ namespace UnitTest
                 };
                 
                 AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                    &AzFramework::Terrain::TerrainDataRequests::ProcessHeightsFromList, inPositions, perPositionCallback, sampler);
+                    &AzFramework::Terrain::TerrainDataRequests::QueryList, inPositions,
+                    AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Heights, perPositionCallback, sampler);
             }
         );
     }
@@ -292,17 +299,18 @@ namespace UnitTest
                 };
 
                 AZStd::semaphore completionEvent;
-                auto completionCallback = [&completionEvent](AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext>)
+                auto completionCallback = [&completionEvent](AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext>)
                 {
                     completionEvent.release();
                 };
 
-                AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams> asyncParams
-                    = AZStd::make_shared<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams>();
-                asyncParams->m_desiredNumberOfJobs = AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams::NumJobsMax;
+                AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> asyncParams
+                    = AZStd::make_shared<AzFramework::Terrain::QueryAsyncParams>();
+                asyncParams->m_desiredNumberOfJobs = AzFramework::Terrain::QueryAsyncParams::NumJobsMax;
                 asyncParams->m_completionCallback = completionCallback;
                 AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                    &AzFramework::Terrain::TerrainDataRequests::ProcessHeightsFromListAsync, inPositions, perPositionCallback, sampler, asyncParams);
+                    &AzFramework::Terrain::TerrainDataRequests::QueryListAsync, inPositions,
+                    AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Heights, perPositionCallback, sampler, asyncParams);
 
                 completionEvent.acquire();
             }
@@ -366,8 +374,11 @@ namespace UnitTest
                 };
                 
                 AZ::Vector2 stepSize = AZ::Vector2(queryResolution);
+                AzFramework::Terrain::TerrainQueryRegion queryRegion =
+                    AzFramework::Terrain::TerrainQueryRegion::CreateFromAabbAndStepSize(worldBounds, stepSize);
                 AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                    &AzFramework::Terrain::TerrainDataRequests::ProcessNormalsFromRegion, worldBounds, stepSize, perPositionCallback, sampler);
+                    &AzFramework::Terrain::TerrainDataRequests::QueryRegion, queryRegion,
+                    AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Normals, perPositionCallback, sampler);
             }
         );
     }
@@ -395,19 +406,22 @@ namespace UnitTest
                 };
 
                 AZStd::semaphore completionEvent;
-                auto completionCallback = [&completionEvent](AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext>)
+                auto completionCallback = [&completionEvent](AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext>)
                 {
                     completionEvent.release();
                 };
 
-                AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams> asyncParams
-                    = AZStd::make_shared<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams>();
-                asyncParams->m_desiredNumberOfJobs = AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams::NumJobsMax;
+                AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> asyncParams
+                    = AZStd::make_shared<AzFramework::Terrain::QueryAsyncParams>();
+                asyncParams->m_desiredNumberOfJobs = AzFramework::Terrain::QueryAsyncParams::NumJobsMax;
                 asyncParams->m_completionCallback = completionCallback;
 
                 AZ::Vector2 stepSize = AZ::Vector2(queryResolution);
+                AzFramework::Terrain::TerrainQueryRegion queryRegion =
+                    AzFramework::Terrain::TerrainQueryRegion::CreateFromAabbAndStepSize(worldBounds, stepSize);
                 AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                    &AzFramework::Terrain::TerrainDataRequests::ProcessNormalsFromRegionAsync, worldBounds, stepSize, perPositionCallback, sampler, asyncParams);
+                    &AzFramework::Terrain::TerrainDataRequests::QueryRegionAsync, queryRegion,
+                    AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Normals, perPositionCallback, sampler, asyncParams);
 
                 completionEvent.acquire();
             }
@@ -439,7 +453,8 @@ namespace UnitTest
                 };
                 
                 AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                    &AzFramework::Terrain::TerrainDataRequests::ProcessNormalsFromList, inPositions, perPositionCallback, sampler);
+                    &AzFramework::Terrain::TerrainDataRequests::QueryList, inPositions,
+                    AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Normals, perPositionCallback, sampler);
             }
         );
     }
@@ -469,17 +484,18 @@ namespace UnitTest
                 };
 
                 AZStd::semaphore completionEvent;
-                auto completionCallback = [&completionEvent](AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext>)
+                auto completionCallback = [&completionEvent](AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext>)
                 {
                     completionEvent.release();
                 };
 
-                AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams> asyncParams
-                    = AZStd::make_shared<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams>();
-                asyncParams->m_desiredNumberOfJobs = AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams::NumJobsMax;
+                AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> asyncParams
+                    = AZStd::make_shared<AzFramework::Terrain::QueryAsyncParams>();
+                asyncParams->m_desiredNumberOfJobs = AzFramework::Terrain::QueryAsyncParams::NumJobsMax;
                 asyncParams->m_completionCallback = completionCallback;
                 AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                    &AzFramework::Terrain::TerrainDataRequests::ProcessNormalsFromListAsync, inPositions, perPositionCallback, sampler, asyncParams);
+                    &AzFramework::Terrain::TerrainDataRequests::QueryListAsync, inPositions,
+                    AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Normals, perPositionCallback, sampler, asyncParams);
 
                 completionEvent.acquire();
             }
@@ -541,8 +557,11 @@ namespace UnitTest
                 };
                 
                 AZ::Vector2 stepSize = AZ::Vector2(queryResolution);
+                AzFramework::Terrain::TerrainQueryRegion queryRegion =
+                    AzFramework::Terrain::TerrainQueryRegion::CreateFromAabbAndStepSize(worldBounds, stepSize);
                 AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                    &AzFramework::Terrain::TerrainDataRequests::ProcessSurfaceWeightsFromRegion, worldBounds, stepSize, perPositionCallback, sampler);
+                    &AzFramework::Terrain::TerrainDataRequests::QueryRegion, queryRegion,
+                    AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::SurfaceData, perPositionCallback, sampler);
             }
         );
     }
@@ -570,19 +589,22 @@ namespace UnitTest
                 };
 
                 AZStd::semaphore completionEvent;
-                auto completionCallback = [&completionEvent](AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext>)
+                auto completionCallback = [&completionEvent](AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext>)
                 {
                     completionEvent.release();
                 };
 
-                AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams> asyncParams
-                    = AZStd::make_shared<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams>();
-                asyncParams->m_desiredNumberOfJobs = AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams::NumJobsMax;
+                AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> asyncParams
+                    = AZStd::make_shared<AzFramework::Terrain::QueryAsyncParams>();
+                asyncParams->m_desiredNumberOfJobs = AzFramework::Terrain::QueryAsyncParams::NumJobsMax;
                 asyncParams->m_completionCallback = completionCallback;
 
                 AZ::Vector2 stepSize = AZ::Vector2(queryResolution);
+                AzFramework::Terrain::TerrainQueryRegion queryRegion =
+                    AzFramework::Terrain::TerrainQueryRegion::CreateFromAabbAndStepSize(worldBounds, stepSize);
                 AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                    &AzFramework::Terrain::TerrainDataRequests::ProcessSurfaceWeightsFromRegionAsync, worldBounds, stepSize, perPositionCallback, sampler, asyncParams);
+                    &AzFramework::Terrain::TerrainDataRequests::QueryRegionAsync, queryRegion,
+                    AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::SurfaceData, perPositionCallback, sampler, asyncParams);
 
                 completionEvent.acquire();
             }
@@ -614,7 +636,8 @@ namespace UnitTest
                 };
                 
                 AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                    &AzFramework::Terrain::TerrainDataRequests::ProcessSurfaceWeightsFromList, inPositions, perPositionCallback, sampler);
+                    &AzFramework::Terrain::TerrainDataRequests::QueryList, inPositions,
+                    AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::SurfaceData, perPositionCallback, sampler);
             }
         );
     }
@@ -644,17 +667,18 @@ namespace UnitTest
                 };
 
                 AZStd::semaphore completionEvent;
-                auto completionCallback = [&completionEvent](AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext>)
+                auto completionCallback = [&completionEvent](AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext>)
                 {
                     completionEvent.release();
                 };
 
-                AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams> asyncParams
-                    = AZStd::make_shared<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams>();
-                asyncParams->m_desiredNumberOfJobs = AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams::NumJobsMax;
+                AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> asyncParams
+                    = AZStd::make_shared<AzFramework::Terrain::QueryAsyncParams>();
+                asyncParams->m_desiredNumberOfJobs = AzFramework::Terrain::QueryAsyncParams::NumJobsMax;
                 asyncParams->m_completionCallback = completionCallback;
                 AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                    &AzFramework::Terrain::TerrainDataRequests::ProcessSurfaceWeightsFromListAsync, inPositions, perPositionCallback, sampler, asyncParams);
+                    &AzFramework::Terrain::TerrainDataRequests::QueryListAsync, inPositions,
+                    AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::SurfaceData, perPositionCallback, sampler, asyncParams);
 
                 completionEvent.acquire();
             }
@@ -716,8 +740,11 @@ namespace UnitTest
                 };
                 
                 AZ::Vector2 stepSize = AZ::Vector2(queryResolution);
+                AzFramework::Terrain::TerrainQueryRegion queryRegion =
+                    AzFramework::Terrain::TerrainQueryRegion::CreateFromAabbAndStepSize(worldBounds, stepSize);
                 AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                    &AzFramework::Terrain::TerrainDataRequests::ProcessSurfacePointsFromRegion, worldBounds, stepSize, perPositionCallback, sampler);
+                    &AzFramework::Terrain::TerrainDataRequests::QueryRegion, queryRegion,
+                    AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::All, perPositionCallback, sampler);
             }
         );
     }
@@ -746,19 +773,22 @@ namespace UnitTest
                 };
 
                 AZStd::semaphore completionEvent;
-                auto completionCallback = [&completionEvent](AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext>)
+                auto completionCallback = [&completionEvent](AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext>)
                 {
                     completionEvent.release();
                 };
 
-                AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams> asyncParams
-                    = AZStd::make_shared<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams>();
-                asyncParams->m_desiredNumberOfJobs = AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams::NumJobsMax;
+                AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> asyncParams
+                    = AZStd::make_shared<AzFramework::Terrain::QueryAsyncParams>();
+                asyncParams->m_desiredNumberOfJobs = AzFramework::Terrain::QueryAsyncParams::NumJobsMax;
                 asyncParams->m_completionCallback = completionCallback;
 
                 AZ::Vector2 stepSize = AZ::Vector2(queryResolution);
+                AzFramework::Terrain::TerrainQueryRegion queryRegion =
+                    AzFramework::Terrain::TerrainQueryRegion::CreateFromAabbAndStepSize(worldBounds, stepSize);
                 AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                    &AzFramework::Terrain::TerrainDataRequests::ProcessSurfacePointsFromRegionAsync, worldBounds, stepSize, perPositionCallback, sampler, asyncParams);
+                    &AzFramework::Terrain::TerrainDataRequests::QueryRegionAsync, queryRegion,
+                    AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::All, perPositionCallback, sampler, asyncParams);
 
                 completionEvent.acquire();
             }
@@ -790,7 +820,8 @@ namespace UnitTest
                 };
                 
                 AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                    &AzFramework::Terrain::TerrainDataRequests::ProcessSurfacePointsFromList, inPositions, perPositionCallback, sampler);
+                    &AzFramework::Terrain::TerrainDataRequests::QueryList, inPositions,
+                    AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::All, perPositionCallback, sampler);
             }
         );
     }
@@ -820,17 +851,18 @@ namespace UnitTest
                 };
 
                 AZStd::semaphore completionEvent;
-                auto completionCallback = [&completionEvent](AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext>)
+                auto completionCallback = [&completionEvent](AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext>)
                 {
                     completionEvent.release();
                 };
 
-                AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams> asyncParams
-                    = AZStd::make_shared<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams>();
-                asyncParams->m_desiredNumberOfJobs = AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams::NumJobsMax;
+                AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> asyncParams
+                    = AZStd::make_shared<AzFramework::Terrain::QueryAsyncParams>();
+                asyncParams->m_desiredNumberOfJobs = AzFramework::Terrain::QueryAsyncParams::NumJobsMax;
                 asyncParams->m_completionCallback = completionCallback;
                 AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                    &AzFramework::Terrain::TerrainDataRequests::ProcessSurfacePointsFromListAsync, inPositions, perPositionCallback, sampler, asyncParams);
+                    &AzFramework::Terrain::TerrainDataRequests::QueryListAsync, inPositions,
+                    AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::All, perPositionCallback, sampler, asyncParams);
 
                 completionEvent.acquire();
             }
@@ -872,18 +904,19 @@ namespace UnitTest
                 for (uint32_t query = 0; query < numParallelQueries; query++)
                 {
                     auto completionCallback =
-                        [&completionEvents, query](AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext>)
+                        [&completionEvents, query](AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext>)
                     {
                         completionEvents[query].release();
                     };
 
-                    AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams> asyncParams =
-                        AZStd::make_shared<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams>();
+                    AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> asyncParams =
+                        AZStd::make_shared<AzFramework::Terrain::QueryAsyncParams>();
                     // Limit each query to 2 threads so that it's possible to run multiple of them simultaneously.
                     asyncParams->m_desiredNumberOfJobs = 2;
                     asyncParams->m_completionCallback = completionCallback;
                     AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                        &AzFramework::Terrain::TerrainDataRequests::ProcessSurfacePointsFromListAsync, inPositions, perPositionCallback,
+                        &AzFramework::Terrain::TerrainDataRequests::QueryListAsync, inPositions,
+                        AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::All, perPositionCallback,
                         sampler, asyncParams);
                 }
 
@@ -1003,7 +1036,8 @@ namespace UnitTest
                 };
 
                 AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                    &AzFramework::Terrain::TerrainDataRequests::ProcessSurfacePointsFromList, inPositions, perPositionCallback, sampler);
+                    &AzFramework::Terrain::TerrainDataRequests::QueryList, inPositions,
+                    AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::All, perPositionCallback, sampler);
             });
     }
 
@@ -1036,17 +1070,18 @@ namespace UnitTest
 
                 AZStd::semaphore completionEvent;
                 auto completionCallback =
-                    [&completionEvent](AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext>)
+                    [&completionEvent](AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext>)
                 {
                     completionEvent.release();
                 };
 
-                AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams> asyncParams =
-                    AZStd::make_shared<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams>();
-                asyncParams->m_desiredNumberOfJobs = AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams::NumJobsMax;
+                AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> asyncParams =
+                    AZStd::make_shared<AzFramework::Terrain::QueryAsyncParams>();
+                asyncParams->m_desiredNumberOfJobs = AzFramework::Terrain::QueryAsyncParams::NumJobsMax;
                 asyncParams->m_completionCallback = completionCallback;
                 AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                    &AzFramework::Terrain::TerrainDataRequests::ProcessSurfacePointsFromListAsync, inPositions, perPositionCallback,
+                    &AzFramework::Terrain::TerrainDataRequests::QueryListAsync, inPositions,
+                    AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::All, perPositionCallback,
                     sampler, asyncParams);
 
                 completionEvent.acquire();
@@ -1088,18 +1123,19 @@ namespace UnitTest
                 for (uint32_t query = 0; query < numParallelQueries; query++)
                 {
                     auto completionCallback =
-                        [&completionEvents, query](AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext>)
+                        [&completionEvents, query](AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext>)
                     {
                         completionEvents[query].release();
                     };
 
-                    AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams> asyncParams =
-                        AZStd::make_shared<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams>();
+                    AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> asyncParams =
+                        AZStd::make_shared<AzFramework::Terrain::QueryAsyncParams>();
                     // Limit each query to 2 threads so that it's possible to run multiple of them simultaneously.
                     asyncParams->m_desiredNumberOfJobs = 2;
                     asyncParams->m_completionCallback = completionCallback;
                     AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                        &AzFramework::Terrain::TerrainDataRequests::ProcessSurfacePointsFromListAsync, inPositions, perPositionCallback,
+                        &AzFramework::Terrain::TerrainDataRequests::QueryListAsync, inPositions,
+                        AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::All, perPositionCallback,
                         sampler, asyncParams);
                 }
 
@@ -1150,7 +1186,8 @@ namespace UnitTest
                             syncThreads.acquire();
 
                             AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-                                &AzFramework::Terrain::TerrainDataRequests::ProcessSurfacePointsFromList, inPositions, perPositionCallback,
+                                &AzFramework::Terrain::TerrainDataRequests::QueryList, inPositions,
+                                AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::All, perPositionCallback,
                                 sampler);
                         });
                 }

+ 34 - 15
Gems/Terrain/Code/Tests/TerrainSystemTest.cpp

@@ -711,7 +711,9 @@ namespace UnitTest
             inPositions.push_back(position);
         }
 
-        terrainSystem->ProcessHeightsFromList(inPositions, perPositionCallback, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR);
+        terrainSystem->QueryList(
+            inPositions, AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Heights, perPositionCallback,
+            AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR);
     }
 
     TEST_F(TerrainSystemTest, TerrainProcessNormalsFromListWithBilinearSamplers)
@@ -799,7 +801,9 @@ namespace UnitTest
             inPositions.push_back(position);
         }
 
-        terrainSystem->ProcessNormalsFromList(inPositions, perPositionCallback, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR);
+        terrainSystem->QueryList(
+            inPositions, AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Normals, perPositionCallback,
+            AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR);
     }
 
     TEST_F(TerrainSystemTest, TerrainProcessHeightsFromRegionWithBilinearSamplers)
@@ -828,8 +832,10 @@ namespace UnitTest
         // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 1 meter intervals.
         auto terrainSystem = CreateAndActivateTerrainSystem(frequencyMeters);
 
-        const AZ::Aabb testRegionBox = AZ::Aabb::CreateFromMinMaxValues(-1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f);
+        // Set up a query region that starts at (-1, -1, -1), queries 2 points in the X and Y direction, and uses a step size of 1.0.
+        // This should query (-1, -1), (0, -1), (-1, 0), and (0, 0).
         const AZ::Vector2 stepSize(1.0f);
+        const AzFramework::Terrain::TerrainQueryRegion queryRegion(AZ::Vector3(-1.0f), 2, 2, stepSize);
 
         const HeightTestRegionPoints testPoints[] = {
             { 0, 0, -2.0f, AZ::Vector2(-1.0f, -1.0f) },
@@ -857,7 +863,9 @@ namespace UnitTest
             EXPECT_EQ(found, true);
         };
 
-        terrainSystem->ProcessHeightsFromRegion(testRegionBox, stepSize, perPositionCallback, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR);
+        terrainSystem->QueryRegion(
+            queryRegion, AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Heights, perPositionCallback,
+            AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR);
     }
 
     TEST_F(TerrainSystemTest, TerrainProcessNormalsFromRegionWithBilinearSamplers)
@@ -886,8 +894,10 @@ namespace UnitTest
         // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 1 meter intervals.
         auto terrainSystem = CreateAndActivateTerrainSystem(frequencyMeters);
 
-        const AZ::Aabb testRegionBox = AZ::Aabb::CreateFromMinMaxValues(-1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f);
+        // Set up a query region that starts at (-1, -1, -1), queries 2 points in the X and Y direction, and uses a step size of 1.0.
+        // This should query (-1, -1), (0, -1), (-1, 0), and (0, 0).
         const AZ::Vector2 stepSize(1.0f);
+        const AzFramework::Terrain::TerrainQueryRegion queryRegion(AZ::Vector3(-1.0f), 2, 2, stepSize);
 
         const NormalTestRegionPoints testPoints[] = {
             { 0, 0, AZ::Vector3(-0.5773f, -0.5773f, 0.5773f), AZ::Vector2(-1.0f, -1.0f) },
@@ -917,7 +927,9 @@ namespace UnitTest
             EXPECT_EQ(found, true);
         };
 
-        terrainSystem->ProcessNormalsFromRegion(testRegionBox, stepSize, perPositionCallback, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR);
+        terrainSystem->QueryRegion(
+            queryRegion, AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Normals, perPositionCallback,
+            AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR);
     }
 
     TEST_F(TerrainSystemTest, TerrainProcessSurfaceWeightsFromRegion)
@@ -935,8 +947,9 @@ namespace UnitTest
         const float queryResolution = 1.0f;
         auto terrainSystem = CreateAndActivateTerrainSystem(queryResolution);
 
-        const AZ::Aabb testRegionBox = AZ::Aabb::CreateFromMinMaxValues(-3.0f, -3.0f, -1.0f, 3.0f, 3.0f, 1.0f);
+        // Set up a query region that starts at (-3, -3, -1), queries 6 points in the X and Y direction, and uses a step size of 1.0.
         const AZ::Vector2 stepSize(1.0f);
+        const AzFramework::Terrain::TerrainQueryRegion queryRegion(AZ::Vector3(-3.0f, -3.0f, -1.0f), 6, 6, stepSize);
 
         AzFramework::SurfaceData::SurfaceTagWeightList expectedTags;
         SetupSurfaceWeightMocks(entity.get(), expectedTags);
@@ -963,7 +976,9 @@ namespace UnitTest
             }
         };
 
-        terrainSystem->ProcessSurfaceWeightsFromRegion(testRegionBox, stepSize, perPositionCallback, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR);
+        terrainSystem->QueryRegion(
+            queryRegion, AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::SurfaceData, perPositionCallback,
+            AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR);
     }
 
     TEST_F(TerrainSystemTest, TerrainProcessSurfacePointsFromRegion)
@@ -981,8 +996,9 @@ namespace UnitTest
         const float queryResolution = 1.0f;
         auto terrainSystem = CreateAndActivateTerrainSystem(queryResolution);
 
-        const AZ::Aabb testRegionBox = AZ::Aabb::CreateFromMinMaxValues(-3.0f, -3.0f, -1.0f, 3.0f, 3.0f, 1.0f);
+        // Set up a query region that starts at (-3, -3, -1), queries 6 points in the X and Y direction, and uses a step size of 1.0.
         const AZ::Vector2 stepSize(1.0f);
+        const AzFramework::Terrain::TerrainQueryRegion queryRegion(AZ::Vector3(-3.0f, -3.0f, -1.0f), 6, 6, stepSize);
 
         AzFramework::SurfaceData::SurfaceTagWeightList expectedTags;
         SetupSurfaceWeightMocks(entity.get(), expectedTags);
@@ -1013,7 +1029,9 @@ namespace UnitTest
             }
         };
 
-        terrainSystem->ProcessSurfacePointsFromRegion(testRegionBox, stepSize, perPositionCallback, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT);
+        terrainSystem->QueryRegion(
+            queryRegion, AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::All, perPositionCallback,
+            AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT);
     }
 
     TEST_F(TerrainSystemTest, TerrainProcessAsyncCancellation)
@@ -1059,21 +1077,22 @@ namespace UnitTest
 
         // Setup the completion callback so we can check that the entire request was cancelled.
         AZStd::semaphore asyncRequestCompletedEvent;
-        auto completionCallback = [&asyncRequestCompletedEvent](AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext> terrainJobContext)
+        auto completionCallback = [&asyncRequestCompletedEvent](AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> terrainJobContext)
         {
             EXPECT_TRUE(terrainJobContext->IsCancelled());
             asyncRequestCompletedEvent.release();
         };
 
         // Invoke the async request.
-        AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams> asyncParams
-            = AZStd::make_shared<AzFramework::Terrain::TerrainDataRequests::ProcessAsyncParams>();
+        AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> asyncParams
+            = AZStd::make_shared<AzFramework::Terrain::QueryAsyncParams>();
         // Only use one job. We're using a lot of handshaking logic to ensure we process the main thread test logic and the callback logic
         // in the exact order we want for the test, and this logic assumes only one job is running.
         asyncParams->m_desiredNumberOfJobs = 1;
         asyncParams->m_completionCallback = completionCallback;
-        AZStd::shared_ptr<AzFramework::Terrain::TerrainDataRequests::TerrainJobContext> terrainJobContext
-            = terrainSystem->ProcessHeightsFromListAsync(inPositions, perPositionCallback, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR, asyncParams);
+        AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext> terrainJobContext = terrainSystem->QueryListAsync(
+            inPositions, AzFramework::Terrain::TerrainDataRequests::TerrainDataMask::Heights, perPositionCallback,
+            AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR, asyncParams);
 
         // Wait until the async request has started before cancelling it.
         asyncRequestStartedEvent.acquire();