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

[Terrain] Remove xy terrain world bounds (#10977)

This PR removes the X/Y bounds from the terrain system since they are no longer needed by any underlying system. The terrain system only needs to know the z bounds to keep the height ranges within reasonable limits for numerical precision. Most of the changes involved relying on a new bus to get the height bounds instead of the world bounds.

A getter for the world bounds still exists, but instead of returning a fixed bounds set by the user, it's simply the combination of the bounds of all terrain areas in the system. This can be used to early out in cases where a point is outside any terrain areas.

Other small unrelated fixes:

- There was an issue with the ray tracing terrain mesh where the wrong bounds were being clamped which could lead to accessing memory out of range.
- The Terrain feature processor had some dead code which has now been removed.
Ken Pruiksma 3 жил өмнө
parent
commit
2bccbe0cba
22 өөрчлөгдсөн 288 нэмэгдсэн , 234 устгасан
  1. 13 13
      AutomatedTesting/Gem/PythonTests/Terrain/EditorScripts/Terrain_World_ConfigurationWorks.py
  2. 0 5
      Code/Editor/GameExporter.cpp
  3. 25 0
      Code/Framework/AzFramework/AzFramework/Terrain/TerrainDataRequestBus.cpp
  4. 36 3
      Code/Framework/AzFramework/AzFramework/Terrain/TerrainDataRequestBus.h
  5. 2 1
      Code/Framework/AzFramework/Tests/Mocks/Terrain/MockTerrainDataRequestBus.h
  6. 5 6
      Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.cpp
  7. 1 2
      Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.h
  8. 28 28
      Gems/Terrain/Code/Source/Components/TerrainWorldComponent.cpp
  9. 9 9
      Gems/Terrain/Code/Source/Components/TerrainWorldComponent.h
  10. 8 35
      Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.cpp
  11. 1 4
      Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.h
  12. 17 17
      Gems/Terrain/Code/Source/TerrainRenderer/TerrainMeshManager.cpp
  13. 1 2
      Gems/Terrain/Code/Source/TerrainRenderer/TerrainMeshManager.h
  14. 90 75
      Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp
  15. 11 4
      Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h
  16. 4 4
      Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp
  17. 1 5
      Gems/Terrain/Code/Tests/TerrainHeightGradientListTests.cpp
  18. 8 6
      Gems/Terrain/Code/Tests/TerrainSystemSettingsTests.cpp
  19. 7 2
      Gems/Terrain/Code/Tests/TerrainSystemTest.cpp
  20. 8 6
      Gems/Terrain/Code/Tests/TerrainTestFixtures.cpp
  21. 2 2
      Gems/Terrain/Code/Tests/TerrainTestFixtures.h
  22. 11 5
      Gems/Terrain/Code/Tests/TerrainWorldComponentTests.cpp

+ 13 - 13
AutomatedTesting/Gem/PythonTests/Terrain/EditorScripts/Terrain_World_ConfigurationWorks.py

@@ -61,8 +61,8 @@ def Terrain_World_ConfigurationWorks():
     import azlmbr.terrain as terrain
     import math
 
-    SET_BOX_X_SIZE = 2048.0
-    SET_BOX_Y_SIZE = 2048.0
+    SET_BOX_X_SIZE = 1024.0
+    SET_BOX_Y_SIZE = 1024.0
     SET_BOX_Z_SIZE = 100.0
     CLAMP = 1
 
@@ -91,17 +91,17 @@ def Terrain_World_ConfigurationWorks():
         general.idle_wait_frames(1)
 
         # 5) Set the base Terrain World values
-        world_bounds_max = azmath.Vector3(1100.0, 1100.0, 1100.0)
-        world_bounds_min = azmath.Vector3(10.0, 10.0, 10.0)
+        world_height_min = 10.0
+        world_height_max = 1100.0
         height_query_resolution = 1.0
-        hydra.set_component_property_value(terrain_world_component, "Configuration|World Bounds (Max)", world_bounds_max)
-        hydra.set_component_property_value(terrain_world_component, "Configuration|World Bounds (Min)", world_bounds_min)
+        hydra.set_component_property_value(terrain_world_component, "Configuration|Min Height", world_height_min)
+        hydra.set_component_property_value(terrain_world_component, "Configuration|Max Height", world_height_max)
         hydra.set_component_property_value(terrain_world_component, "Configuration|Height Query Resolution (m)", height_query_resolution)
-        world_max = hydra.get_component_property_value(terrain_world_component, "Configuration|World Bounds (Max)")
-        world_min = hydra.get_component_property_value(terrain_world_component, "Configuration|World Bounds (Min)")
+        world_min = hydra.get_component_property_value(terrain_world_component, "Configuration|Min Height")
+        world_max = hydra.get_component_property_value(terrain_world_component, "Configuration|Max Height")
         world_query = hydra.get_component_property_value(terrain_world_component, "Configuration|Height Query Resolution (m)")
-        Report.result(Tests.bounds_max_changed, world_max == world_bounds_max)
-        Report.result(Tests.bounds_min_changed, world_min == world_bounds_min)
+        Report.result(Tests.bounds_min_changed, world_min == world_height_min)
+        Report.result(Tests.bounds_max_changed, world_max == world_height_max)
         Report.result(Tests.height_query_changed, world_query == height_query_resolution)
 
         # 6) Change the Axis Aligned Box Shape dimensions
@@ -137,14 +137,14 @@ def Terrain_World_ConfigurationWorks():
         terrainExists = not terrain.TerrainDataRequestBus(bus.Broadcast, 'GetIsHole', azmath.Vector3(10.0, 10.0, 0.0), CLAMP)
         Report.result(Tests.terrain_exists, terrainExists)
 
-        terrainExists = not terrain.TerrainDataRequestBus(bus.Broadcast, 'GetIsHole', azmath.Vector3(1100.0, 1100.0, 0.0), CLAMP)
+        terrainExists = not terrain.TerrainDataRequestBus(bus.Broadcast, 'GetIsHole', azmath.Vector3(1023.0, 1023.0, 0.0), CLAMP)
         Report.result(Tests.terrain_exists, terrainExists)
 
         # 12) Check terrain does not exist at a known position outside the world
-        terrainDoesNotExist = terrain.TerrainDataRequestBus(bus.Broadcast, 'GetIsHole', azmath.Vector3(1101.0, 1101.0, 0.0), CLAMP)
+        terrainDoesNotExist = terrain.TerrainDataRequestBus(bus.Broadcast, 'GetIsHole', azmath.Vector3(1025.0, 1025.0, 0.0), CLAMP)
         Report.result(Tests.terrain_does_not_exist, terrainDoesNotExist)
 
-        terrainDoesNotExist = terrain.TerrainDataRequestBus(bus.Broadcast, 'GetIsHole', azmath.Vector3(9.0, 9.0, 0.0), CLAMP)
+        terrainDoesNotExist = terrain.TerrainDataRequestBus(bus.Broadcast, 'GetIsHole', azmath.Vector3(-1.0, -1.0, 0.0), CLAMP)
         Report.result(Tests.terrain_does_not_exist, terrainDoesNotExist)
 
         # 13) Check height value is the expected one when query resolution is changed

+ 0 - 5
Code/Editor/GameExporter.cpp

@@ -315,11 +315,6 @@ void CGameExporter::ExportLevelInfo(const QString& path)
 
     QString levelName = pEditor->GetGameEngine()->GetLevelPath();
     root->setAttr("Name", levelName.toUtf8().data());
-    auto terrain = AzFramework::Terrain::TerrainDataRequestBus::FindFirstHandler();
-    const AZ::Aabb terrainAabb = terrain ? terrain->GetTerrainAabb() : AZ::Aabb::CreateFromPoint(AZ::Vector3::CreateZero());
-    const float terrainGridResolution = terrain ? terrain->GetTerrainHeightQueryResolution() : 1.0f;
-    const int compiledHeightmapSize = static_cast<int>(terrainAabb.GetXExtent() / terrainGridResolution);
-    root->setAttr("HeightmapSize", compiledHeightmapSize);
 
     //////////////////////////////////////////////////////////////////////////
     // Save LevelInfo file.

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

@@ -12,6 +12,28 @@
 
 namespace AzFramework::Terrain
 {
+
+    void FloatRange::Reflect(AZ::ReflectContext* context)
+    {
+        if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            serializeContext->Class<FloatRange>()
+                ->Version(1)
+                ->Field("Min", &FloatRange::m_min)
+                ->Field("Max", &FloatRange::m_max)
+                ;
+        }
+
+        if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
+        {
+            behaviorContext->Class<FloatRange>()
+                ->Property("min", BehaviorValueProperty(&FloatRange::m_min))
+                ->Property("max", BehaviorValueProperty(&FloatRange::m_max))
+                ;
+        }
+
+    }
+
     TerrainQueryRegion::TerrainQueryRegion(
         const AZ::Vector3& startPoint, size_t numPointsX, size_t numPointsY, const AZ::Vector2& stepSize)
         : m_startPoint(startPoint)
@@ -98,6 +120,8 @@ namespace AzFramework::Terrain
 
     void TerrainDataRequests::Reflect(AZ::ReflectContext* context)
     {
+        FloatRange::Reflect(context);
+
         if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
         {
             // Register all the tuple types used below so that they marshal to/from python correctly.
@@ -128,6 +152,7 @@ namespace AzFramework::Terrain
                     "SetTerrainSurfaceDataQueryResolution",
                     &AzFramework::Terrain::TerrainDataRequestBus::Events::SetTerrainSurfaceDataQueryResolution)
                 ->Event("GetTerrainAabb", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetTerrainAabb)
+                ->Event("GetTerrainHeightBounds", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetTerrainHeightBounds)
                 ->Event(
                     "GetHeight",
                     [](AzFramework::Terrain::TerrainDataRequests* handler, const AZ::Vector3& position,

+ 36 - 3
Code/Framework/AzFramework/AzFramework/Terrain/TerrainDataRequestBus.h

@@ -26,6 +26,36 @@ 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;
 
+        struct FloatRange
+        {
+            AZ_TYPE_INFO(FloatRange, "{7E6319B6-1409-4865-8AD1-6F68272A94E9}");
+
+            static void Reflect(AZ::ReflectContext* context);
+
+            float m_min = 0.0f;
+            float m_max = 0.0f;
+
+            bool IsValid() const
+            {
+                return m_min <= m_max;
+            }
+
+            static FloatRange CreateNull()
+            {
+                return { 0.0f, -1.0f };
+            }
+
+            bool operator==(const FloatRange& other) const
+            {
+                return m_min == other.m_min && m_max == other.m_max;
+            }
+
+            bool operator!=(const FloatRange& other) const
+            {
+                return !(*this == other);
+            }
+        };
+
         //! Helper structure that defines a query region to use with the QueryRegion / QueryRegionAsync APIs.
         struct TerrainQueryRegion
         {
@@ -159,8 +189,11 @@ namespace AzFramework
             virtual float GetTerrainSurfaceDataQueryResolution() const = 0;
             virtual void SetTerrainSurfaceDataQueryResolution(float queryResolution) = 0;
 
+            // Returns a bounding box that contains all current terrain areas. There may still be areas inside the bounds which contain no terrain.
             virtual AZ::Aabb GetTerrainAabb() const = 0;
-            virtual void SetTerrainAabb(const AZ::Aabb& worldBounds) = 0;
+
+            virtual FloatRange GetTerrainHeightBounds() const = 0;
+            virtual void SetTerrainHeightBounds(const FloatRange& heightRange) = 0;
 
             // Returns true if any terrain area spawner intersects with the provided bounds
             virtual bool TerrainAreaExistsInBounds(const AZ::Aabb& bounds) const = 0;
@@ -274,14 +307,14 @@ namespace AzFramework
                 SurfacePointRegionFillCallback perPositionCallback,
                 Sampler sampleFilter = Sampler::DEFAULT) const = 0;
 
-            //! Get the terrain raycast entity context id.
+            //! Get the terrain ray cast entity context id.
             virtual EntityContextId GetTerrainRaycastEntityContextId() const = 0;
 
             //! Given a ray, return the closest intersection with terrain.
             virtual RenderGeometry::RayResult GetClosestIntersection(const RenderGeometry::RayRequest& ray) const = 0;
 
             //! Asynchronous versions of the various 'Query*' API functions declared above.
-            //! It's the responsibility of the caller to ensure all callbacks are threadsafe.
+            //! It's the responsibility of the caller to ensure all callbacks are thread-safe.
             virtual AZStd::shared_ptr<TerrainJobContext> QueryListAsync(
                 const AZStd::span<const AZ::Vector3>& inPositions,
                 TerrainDataMask requestedData,

+ 2 - 1
Code/Framework/AzFramework/Tests/Mocks/Terrain/MockTerrainDataRequestBus.h

@@ -54,7 +54,8 @@ namespace UnitTest
         MOCK_CONST_METHOD0(GetTerrainSurfaceDataQueryResolution, float());
         MOCK_METHOD1(SetTerrainSurfaceDataQueryResolution, void(float));
         MOCK_CONST_METHOD0(GetTerrainAabb, AZ::Aabb());
-        MOCK_METHOD1(SetTerrainAabb, void(const AZ::Aabb&));
+        MOCK_CONST_METHOD0(GetTerrainHeightBounds, AzFramework::Terrain::FloatRange());
+        MOCK_METHOD1(SetTerrainHeightBounds, void(const AzFramework::Terrain::FloatRange&));
         MOCK_CONST_METHOD1(TerrainAreaExistsInBounds, bool(const AZ::Aabb&));
         MOCK_CONST_METHOD3(GetHeight, float(const AZ::Vector3&, Sampler, bool*));
         MOCK_CONST_METHOD3(GetHeightFromVector2, float(const AZ::Vector2&, Sampler, bool*));

+ 5 - 6
Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.cpp

@@ -200,7 +200,7 @@ namespace Terrain
         }
 
         const float height = AZ::Lerp(m_cachedShapeBounds.GetMin().GetZ(), m_cachedShapeBounds.GetMax().GetZ(), maxSample);
-        outPosition.Set(inPosition.GetX(), inPosition.GetY(), AZ::GetClamp(height, m_cachedMinWorldHeight, m_cachedMaxWorldHeight));
+        outPosition.Set(inPosition.GetX(), inPosition.GetY(), AZ::GetClamp(height, m_cachedHeightBounds.m_min, m_cachedHeightBounds.m_max));
     }
 
     void TerrainHeightGradientListComponent::GetHeights(
@@ -259,7 +259,7 @@ namespace Terrain
                 {
                     const float height =
                         AZ::Lerp(m_cachedShapeBounds.GetMin().GetZ(), m_cachedShapeBounds.GetMax().GetZ(), maxValueSamples[index]);
-                    inOutPositionList[index].SetZ(AZ::GetClamp(height, m_cachedMinWorldHeight, m_cachedMaxWorldHeight));
+                    inOutPositionList[index].SetZ(AZ::GetClamp(height, m_cachedHeightBounds.m_min, m_cachedHeightBounds.m_max));
                 }
             }
         }
@@ -281,9 +281,9 @@ namespace Terrain
             shapeBounds, GetEntityId(), &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb);
 
         // Get the height range of the entire world
-        AZ::Aabb worldBounds = AZ::Aabb::CreateNull();
+        AzFramework::Terrain::FloatRange heightBounds = AzFramework::Terrain::FloatRange::CreateNull();
         AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
-            worldBounds, &AzFramework::Terrain::TerrainDataRequestBus::Events::GetTerrainAabb);
+            heightBounds, &AzFramework::Terrain::TerrainDataRequestBus::Events::GetTerrainHeightBounds);
 
         // Ensure that we only change our cached data and terrain registration status when no queries are actively running.
         {
@@ -292,8 +292,7 @@ namespace Terrain
             m_cachedShapeBounds = shapeBounds;
 
             // Save off the min/max heights so that we don't have to re-query them on every single height query.
-            m_cachedMinWorldHeight = worldBounds.GetMin().GetZ();
-            m_cachedMaxWorldHeight = worldBounds.GetMax().GetZ();
+            m_cachedHeightBounds = heightBounds;
         }
 
         // We specifically refresh this outside of the queryMutex lock to avoid lock inversion deadlocks. These can occur if one thread

+ 1 - 2
Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.h

@@ -90,8 +90,7 @@ namespace Terrain
     private:
         TerrainHeightGradientListConfig m_configuration;
 
-        float m_cachedMinWorldHeight{ 0.0f };
-        float m_cachedMaxWorldHeight{ 0.0f };
+        AzFramework::Terrain::FloatRange m_cachedHeightBounds{ 0.0f, 0.0f };
         AZ::Aabb m_cachedShapeBounds;
 
         LmbrCentral::DependencyMonitor m_dependencyMonitor;

+ 28 - 28
Gems/Terrain/Code/Source/Components/TerrainWorldComponent.cpp

@@ -13,7 +13,6 @@
 #include <AzCore/RTTI/BehaviorContext.h>
 #include <AzCore/Serialization/EditContext.h>
 #include <AzCore/Serialization/SerializeContext.h>
-#include <AzFramework/Terrain/TerrainDataRequestBus.h>
 
 namespace Terrain
 {
@@ -28,32 +27,29 @@ namespace Terrain
         AZ_Assert(configInstance, "Output value for JsonTerrainWorldConfigSerializer can't be null.");
         
         JSR::ResultCode result(JSR::Tasks::ReadField);
-        
-        result.Combine(ContinueLoadingFromJsonObjectField(
-            &configInstance->m_worldMin, azrtti_typeid<decltype(configInstance->m_worldMin)>(), inputValue, "WorldMin", context));
-        
-        result.Combine(ContinueLoadingFromJsonObjectField(
-            &configInstance->m_worldMax, azrtti_typeid<decltype(configInstance->m_worldMax)>(), inputValue, "WorldMax", context));
-
-        result.Combine(ContinueLoadingFromJsonObjectField(
-            &configInstance->m_surfaceDataQueryResolution, azrtti_typeid<decltype(configInstance->m_surfaceDataQueryResolution)>(),
-            inputValue, "SurfaceDataQueryResolution", context));
 
-        rapidjson::Value::ConstMemberIterator itr = inputValue.FindMember("HeightQueryResolution");
-        if (itr != inputValue.MemberEnd())
+        auto arrayFloatToSingleValue = [&](const char* oldName, const char* newName, auto& dataRef, uint32_t index)
         {
-            if (itr->value.IsArray())
+            rapidjson::Value::ConstMemberIterator itr = inputValue.FindMember(oldName);
+            if (itr != inputValue.MemberEnd() && itr->value.IsArray())
             {
-                // Version 1 stored a Vector2 (serialized as a json array) to have a separate x and y
-                // query resolution. Now this is only one value, so just take the x value from the Vector2.
-                configInstance->m_heightQueryResolution = itr->value.GetArray().Begin()->GetFloat();
+                dataRef = itr->value.GetArray()[index].GetFloat();
             }
             else
             {
                 result.Combine(ContinueLoadingFromJsonObjectField(
-                    &configInstance->m_heightQueryResolution, azrtti_typeid<decltype(configInstance->m_heightQueryResolution)>(), inputValue, "HeightQueryResolution", context));
+                    &dataRef, azrtti_typeid<decltype(dataRef)>(), inputValue, rapidjson::GenericStringRef<char>(newName), context));
             }
-        }
+        };
+
+        arrayFloatToSingleValue("WorldMin", "MinHeight", configInstance->m_minHeight, 2);
+        arrayFloatToSingleValue("WorldMax", "MaxHeight", configInstance->m_maxHeight, 2);
+
+        arrayFloatToSingleValue("HeightQueryResolution", "HeightQueryResolution", configInstance->m_heightQueryResolution, 0);
+
+        result.Combine(ContinueLoadingFromJsonObjectField(
+            &configInstance->m_surfaceDataQueryResolution, azrtti_typeid<decltype(configInstance->m_surfaceDataQueryResolution)>(),
+            inputValue, "SurfaceDataQueryResolution", context));
 
         return context.Report(result,
             result.GetProcessing() != JSR::Processing::Halted ?
@@ -74,9 +70,9 @@ namespace Terrain
         if (serialize)
         {
             serialize->Class<TerrainWorldConfig, AZ::ComponentConfig>()
-                ->Version(3)
-                ->Field("WorldMin", &TerrainWorldConfig::m_worldMin)
-                ->Field("WorldMax", &TerrainWorldConfig::m_worldMax)
+                ->Version(4)
+                ->Field("MinHeight", &TerrainWorldConfig::m_minHeight)
+                ->Field("MaxHeight", &TerrainWorldConfig::m_maxHeight)
                 ->Field("HeightQueryResolution", &TerrainWorldConfig::m_heightQueryResolution)
                 ->Field("SurfaceDataQueryResolution", &TerrainWorldConfig::m_surfaceDataQueryResolution)
             ;
@@ -90,14 +86,18 @@ namespace Terrain
                         ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
                         ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
 
-                    ->DataElement(AZ::Edit::UIHandlers::Default, &TerrainWorldConfig::m_worldMin, "World Bounds (Min)", "")
+                    ->DataElement(AZ::Edit::UIHandlers::Default, &TerrainWorldConfig::m_minHeight, "Min Height", "")
+                        ->Attribute(AZ::Edit::Attributes::SoftMin, -1000.0f)
+                        ->Attribute(AZ::Edit::Attributes::SoftMax, 1000.0f)
                         ->Attribute(AZ::Edit::Attributes::Min, -65536.0f)
                         ->Attribute(AZ::Edit::Attributes::Max, 65536.0f)
-                        ->Attribute(AZ::Edit::Attributes::ChangeValidate, &TerrainWorldConfig::ValidateBoundsMin)
-                    ->DataElement(AZ::Edit::UIHandlers::Default, &TerrainWorldConfig::m_worldMax, "World Bounds (Max)", "")
+                        ->Attribute(AZ::Edit::Attributes::ChangeValidate, &TerrainWorldConfig::ValidateHeightMin)
+                    ->DataElement(AZ::Edit::UIHandlers::Default, &TerrainWorldConfig::m_maxHeight, "Max Height", "")
+                        ->Attribute(AZ::Edit::Attributes::SoftMin, -1000.0f)
+                        ->Attribute(AZ::Edit::Attributes::SoftMax, 1000.0f)
                         ->Attribute(AZ::Edit::Attributes::Min, -65536.0f)
                         ->Attribute(AZ::Edit::Attributes::Max, 65536.0f)
-                        ->Attribute(AZ::Edit::Attributes::ChangeValidate, &TerrainWorldConfig::ValidateBoundsMax)
+                        ->Attribute(AZ::Edit::Attributes::ChangeValidate, &TerrainWorldConfig::ValidateHeightMax)
                     ->DataElement(
                         AZ::Edit::UIHandlers::Default, &TerrainWorldConfig::m_heightQueryResolution, "Height Query Resolution (m)", "")
                         ->Attribute(AZ::Edit::Attributes::Min, 0.1f)
@@ -154,8 +154,8 @@ namespace Terrain
         TerrainSystemServiceRequestBus::Broadcast(&TerrainSystemServiceRequestBus::Events::Activate);
 
         AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
-            &AzFramework::Terrain::TerrainDataRequestBus::Events::SetTerrainAabb,
-            AZ::Aabb::CreateFromMinMax(m_configuration.m_worldMin, m_configuration.m_worldMax));
+            &AzFramework::Terrain::TerrainDataRequestBus::Events::SetTerrainHeightBounds,
+            AzFramework::Terrain::FloatRange({ m_configuration.m_minHeight, m_configuration.m_maxHeight }));
         AzFramework::Terrain::TerrainDataRequestBus::Broadcast(
             &AzFramework::Terrain::TerrainDataRequestBus::Events::SetTerrainHeightQueryResolution, m_configuration.m_heightQueryResolution);
         AzFramework::Terrain::TerrainDataRequestBus::Broadcast(

+ 9 - 9
Gems/Terrain/Code/Source/Components/TerrainWorldComponent.h

@@ -43,28 +43,28 @@ namespace Terrain
         AZ_RTTI(TerrainWorldConfig, "{295844DB-20DD-45B2-94DB-4245D5AE9AFF}", AZ::ComponentConfig);
         static void Reflect(AZ::ReflectContext* context);
 
-        AZ::Vector3 m_worldMin{ 0.0f, 0.0f, 0.0f };
-        AZ::Vector3 m_worldMax{ 1024.0f, 1024.0f, 1024.0f };
+        float m_minHeight{ 0.0f };
+        float m_maxHeight{ 1024.0f };
         float m_heightQueryResolution{ 1.0f };
         float m_surfaceDataQueryResolution{ 1.0f };
 
-        static AZ::Outcome<void, AZStd::string> ValidateBounds(AZ::Vector3& minBounds, AZ::Vector3& maxBounds)
+        static AZ::Outcome<void, AZStd::string> ValidateHeight(float minHeight, float maxHeight)
         {
-            if (!minBounds.IsLessEqualThan(maxBounds))
+            if (minHeight > maxHeight)
             {
-                return AZ::Failure(AZStd::string("World bounds min must be less than max."));
+                return AZ::Failure(AZStd::string("Terrain min height must be less than max height."));
             }
             return AZ::Success();
         }
 
-        AZ::Outcome<void, AZStd::string> ValidateBoundsMin(void* newValue, [[maybe_unused]] const AZ::Uuid& valueType)
+        AZ::Outcome<void, AZStd::string> ValidateHeightMin(void* newValue, [[maybe_unused]] const AZ::Uuid& valueType)
         {
-            return ValidateBounds(*static_cast<AZ::Vector3*>(newValue), m_worldMax);
+            return ValidateHeight(*static_cast<float*>(newValue), m_maxHeight);
         }
 
-        AZ::Outcome<void, AZStd::string> ValidateBoundsMax(void* newValue, [[maybe_unused]] const AZ::Uuid& valueType)
+        AZ::Outcome<void, AZStd::string> ValidateHeightMax(void* newValue, [[maybe_unused]] const AZ::Uuid& valueType)
         {
-            return ValidateBounds(m_worldMin, *static_cast<AZ::Vector3*>(newValue));
+            return ValidateHeight(m_minHeight, *static_cast<float*>(newValue));
         }
     };
 

+ 8 - 35
Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.cpp

@@ -116,44 +116,17 @@ namespace Terrain
 
     void TerrainFeatureProcessor::OnTerrainDataDestroyBegin()
     {
-        m_zBounds = AZ::Vector2::CreateZero();
-        m_dirtyRegion = AZ::Aabb::CreateNull();
+        m_zBounds = {};
     }
     
-    void TerrainFeatureProcessor::OnTerrainDataChanged(const AZ::Aabb& dirtyRegion, TerrainDataChangedMask dataChangedMask)
+    void TerrainFeatureProcessor::OnTerrainDataChanged([[maybe_unused]] const AZ::Aabb& dirtyRegion, TerrainDataChangedMask dataChangedMask)
     {
-        if ((dataChangedMask & (TerrainDataChangedMask::HeightData | TerrainDataChangedMask::Settings)) != 0)
+        if ((dataChangedMask & TerrainDataChangedMask::Settings) != 0)
         {
-            TerrainHeightOrSettingsUpdated(dirtyRegion);
-        }
-    }
-
-    void TerrainFeatureProcessor::TerrainHeightOrSettingsUpdated(const AZ::Aabb& dirtyRegion)
-    {
-        AZ::Aabb worldBounds = AZ::Aabb::CreateNull();
-        AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
-            worldBounds, &AzFramework::Terrain::TerrainDataRequests::GetTerrainAabb);
-
-        const AZ::Aabb& regionToUpdate = dirtyRegion.IsValid() ? dirtyRegion : worldBounds;
+            AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
+                m_zBounds, &AzFramework::Terrain::TerrainDataRequests::GetTerrainHeightBounds);
 
-        m_dirtyRegion.AddAabb(regionToUpdate);
-        m_dirtyRegion.Clamp(worldBounds);
-
-        float queryResolution = 1.0f;
-        AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
-            queryResolution, &AzFramework::Terrain::TerrainDataRequests::GetTerrainHeightQueryResolution);
-
-        AZ::Vector2 worldZBounds = AZ::Vector2
-        (
-            worldBounds.GetMin().GetZ(),
-            worldBounds.GetMax().GetZ()
-        );
-
-        if (m_zBounds != worldZBounds || m_sampleSpacing != queryResolution)
-        {
             m_terrainBoundsNeedUpdate = true;
-            m_zBounds = worldZBounds;
-            m_sampleSpacing = queryResolution;
         }
     }
 
@@ -301,7 +274,7 @@ namespace Terrain
     {
         AZ_PROFILE_FUNCTION(AzRender);
         
-        if (m_zBounds.IsZero())
+        if (m_zBounds.m_min == 0.0f && m_zBounds.m_max == 0.0f)
         {
             return;
         }
@@ -362,8 +335,8 @@ namespace Terrain
             m_terrainBoundsNeedUpdate = false;
 
             WorldShaderData worldData;
-            worldData.m_zMin = m_zBounds.GetX();
-            worldData.m_zMax = m_zBounds.GetY();
+            worldData.m_zMin = m_zBounds.m_min;
+            worldData.m_zMax = m_zBounds.m_max;
             worldData.m_zExtents = worldData.m_zMax - worldData.m_zMin;
 
             auto sceneSrg = GetParentScene()->GetShaderResourceGroup();

+ 1 - 4
Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.h

@@ -91,8 +91,6 @@ namespace Terrain
 
         void PrepareMaterialData();
 
-        void TerrainHeightOrSettingsUpdated(const AZ::Aabb& dirtyRegion);
-
         void ProcessSurfaces(const FeatureProcessor::RenderPacket& process);
 
         void CachePasses();
@@ -111,10 +109,9 @@ namespace Terrain
 
         AZ::RHI::ShaderInputNameIndex m_worldDataIndex = "m_terrainWorldData";
 
-        AZ::Vector2 m_zBounds{ AZ::Vector2::CreateZero() };
+        AzFramework::Terrain::FloatRange m_zBounds;
         AZ::Aabb m_dirtyRegion{ AZ::Aabb::CreateNull() };
         
-        float m_sampleSpacing{ 0.0f };
         bool m_terrainBoundsNeedUpdate{ false };
 
         AZStd::vector<AZ::RPI::RenderPass*> m_passes;

+ 17 - 17
Gems/Terrain/Code/Source/TerrainRenderer/TerrainMeshManager.cpp

@@ -542,9 +542,9 @@ namespace Terrain
     {
         if ((dataChangedMask & (TerrainDataChangedMask::HeightData | TerrainDataChangedMask::Settings)) != 0)
         {
-            AZ::Aabb worldBounds = AZ::Aabb::CreateNull();
+            AzFramework::Terrain::FloatRange heightBounds = AzFramework::Terrain::FloatRange::CreateNull();
             AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
-                worldBounds, &AzFramework::Terrain::TerrainDataRequests::GetTerrainAabb);
+                heightBounds, &AzFramework::Terrain::TerrainDataRequests::GetTerrainHeightBounds);
 
             float queryResolution = 1.0f;
             AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
@@ -552,11 +552,10 @@ namespace Terrain
 
             bool gridSizeChanged = UpdateGridSize(m_config.m_firstLodDistance);
 
-            // Sectors need to be rebuilt if the sample spacing changes.
-            m_rebuildSectors = m_rebuildSectors || (m_sampleSpacing != queryResolution) || gridSizeChanged;
+            // Sectors need to be rebuilt when certain settings change.
+            m_rebuildSectors = m_rebuildSectors || (m_sampleSpacing != queryResolution) || (heightBounds != m_worldHeightBounds) || gridSizeChanged;
 
-            m_worldMinHeight = worldBounds.GetMin().GetZ();
-            m_worldMaxHeight = worldBounds.GetMax().GetZ();
+            m_worldHeightBounds = heightBounds;
             m_sampleSpacing = queryResolution;
 
             if (dirtyRegion.IsValid())
@@ -791,7 +790,7 @@ namespace Terrain
         (size_t xIndex, size_t yIndex, const AzFramework::SurfaceData::SurfacePoint& surfacePoint, bool terrainExists)
         {
             static constexpr float HeightDoesNotExistValue = -1.0f;
-            const float height = surfacePoint.m_position.GetZ() - m_worldMinHeight;
+            const float height = surfacePoint.m_position.GetZ() - m_worldHeightBounds.m_min;
             heights.at(yIndex * querySamplesX + xIndex) = terrainExists ? height : HeightDoesNotExistValue;
             terrainExistsAnywhere = terrainExistsAnywhere || terrainExists;
         };
@@ -812,8 +811,8 @@ namespace Terrain
             return;
         }
 
-        float zExtents = (m_worldMaxHeight - m_worldMinHeight);
-        const float rcpWorldZ = 1.0f / (m_worldMaxHeight - m_worldMinHeight);
+        float zExtents = (m_worldHeightBounds.m_max - m_worldHeightBounds.m_min);
+        const float rcpWorldZ = 1.0f / zExtents;
         const float vertexSpacing2 = request.m_vertexSpacing * 2.0f;
 
         // initialize min/max heights to the max/min possible values so they're immediately updated when a valid point is found.
@@ -916,8 +915,8 @@ namespace Terrain
         {
             float width = (request.m_samplesX - 1) * request.m_vertexSpacing;
             float height = (request.m_samplesY - 1) * request.m_vertexSpacing;
-            AZ::Vector3 aabbMin = AZ::Vector3(request.m_worldStartPosition.GetX(), request.m_worldStartPosition.GetY(), m_worldMinHeight + minHeight);
-            AZ::Vector3 aabbMax = AZ::Vector3(aabbMin.GetX() + width, aabbMin.GetY() + height, m_worldMinHeight + maxHeight);
+            AZ::Vector3 aabbMin = AZ::Vector3(request.m_worldStartPosition.GetX(), request.m_worldStartPosition.GetY(), m_worldHeightBounds.m_min + minHeight);
+            AZ::Vector3 aabbMax = AZ::Vector3(aabbMin.GetX() + width, aabbMin.GetY() + height, m_worldHeightBounds.m_min + maxHeight);
             meshAabb.Set(aabbMin, aabbMax);
         }
     }
@@ -998,8 +997,9 @@ namespace Terrain
 
                 // Check against the area of terrain that could appear in this sector for any terrain areas. If none exist then skip updating the mesh.
                 bool hasTerrain = false;
-                AZ::Vector3 minAabb = AZ::Vector3(sector->m_worldCoord.m_x * gridMeters, sector->m_worldCoord.m_y * gridMeters, m_worldMinHeight);
-                AZ::Aabb sectorBounds = AZ::Aabb::CreateFromMinMax(minAabb, minAabb + AZ::Vector3(gridMeters, gridMeters, m_worldMaxHeight - m_worldMinHeight));
+                AZ::Vector3 minAabb = AZ::Vector3(sector->m_worldCoord.m_x * gridMeters, sector->m_worldCoord.m_y * gridMeters, m_worldHeightBounds.m_min);
+                AZ::Aabb sectorBounds = AZ::Aabb::CreateFromMinMax(minAabb,
+                    minAabb + AZ::Vector3(gridMeters, gridMeters, m_worldHeightBounds.m_max - m_worldHeightBounds.m_min));
                 AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
                     hasTerrain, &AzFramework::Terrain::TerrainDataRequests::TerrainAreaExistsInBounds, sectorBounds);
 
@@ -1035,9 +1035,9 @@ namespace Terrain
             queryResolution, &AzFramework::Terrain::TerrainDataRequests::GetTerrainHeightQueryResolution);
 
         // For now only create a small patch of terrain data for ray tracing around the origin as a test case.
-        AZ::Aabb raytracingBounds = AZ::Aabb::CreateCenterHalfExtents(AZ::Vector3::CreateZero(), AZ::Vector3(RayTracingQuads1D * queryResolution * 0.5f));
-        AZ::Aabb updateBounds = raytracingBounds.GetClamped(bounds);
-        if (!raytracingBounds.IsValid())
+        const AZ::Aabb raytracingBounds = AZ::Aabb::CreateCenterHalfExtents(AZ::Vector3::CreateZero(), AZ::Vector3(RayTracingQuads1D * queryResolution * 0.5f));
+        const AZ::Aabb updateBounds = bounds.GetClamped(raytracingBounds);
+        if (updateBounds.GetXExtent() <= 0.0f || updateBounds.GetYExtent() <= 0.0f)
         {
             // No raytracing data to update.
             return;
@@ -1086,7 +1086,7 @@ namespace Terrain
         uint32_t yMax = yMin + request.m_samplesY;
 
         constexpr uint32_t RayTracingVertices1D = RayTracingQuads1D + 1;
-        float zExtent = m_worldMaxHeight - m_worldMinHeight;
+        float zExtent = m_worldHeightBounds.m_max - m_worldHeightBounds.m_min;
 
         for (uint32_t y = yMin; y < yMax; ++y)
         {

+ 1 - 2
Gems/Terrain/Code/Source/TerrainRenderer/TerrainMeshManager.h

@@ -285,8 +285,7 @@ namespace Terrain
         // Set up the initial camera position impossible to force an update.
         AZ::Vector3 m_cameraPosition = AZ::Vector3::CreateAxisX(AZStd::numeric_limits<float>::max());
 
-        float m_worldMinHeight = 0.0f;
-        float m_worldMaxHeight = 0.0f;
+        AzFramework::Terrain::FloatRange m_worldHeightBounds;
         float m_sampleSpacing = 1.0f;
         AZ::RPI::Material::ChangeId m_lastMaterialChangeId;
 

+ 90 - 75
Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp

@@ -26,7 +26,7 @@ AZ_DEFINE_BUDGET(Terrain);
 
 bool TerrainLayerPriorityComparator::operator()(const AZ::EntityId& layer1id, const AZ::EntityId& layer2id) const
 {
-    // Comparator for insertion/keylookup.
+    // Comparator for insertion/key lookup.
     // Sorts into layer/priority order, highest priority first.
     int32_t priority1 = 0;
     uint32_t layer1 = 0;
@@ -60,10 +60,10 @@ TerrainSystem::TerrainSystem()
     AZ::TickBus::Handler::BusConnect();
 
     m_currentSettings.m_systemActive = false;
-    m_currentSettings.m_worldBounds = AZ::Aabb::CreateNull();
+    m_currentSettings.m_heightRange = AzFramework::Terrain::FloatRange::CreateNull();
 
     m_requestedSettings = m_currentSettings;
-    m_requestedSettings.m_worldBounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-512.0f), AZ::Vector3(512.0f));
+    m_requestedSettings.m_heightRange = { -512.0f, 512.0f };
 
     // Use the global JobManager for terrain jobs (we could create our own dedicated terrain JobManager if needed).
     AZ::JobManagerBus::BroadcastResult(m_terrainJobManager, &AZ::JobManagerEvents::GetManager);
@@ -88,6 +88,7 @@ void TerrainSystem::Activate()
     m_terrainSettingsDirty = true;
     m_terrainSurfacesDirty = true;
     m_requestedSettings.m_systemActive = true;
+    m_cachedAreaBounds = AZ::Aabb::CreateNull();
 
     {
         AZStd::unique_lock<AZStd::shared_mutex> lock(m_areaMutex);
@@ -145,9 +146,9 @@ void TerrainSystem::Deactivate()
         &AzFramework::Terrain::TerrainDataNotificationBus::Events::OnTerrainDataDestroyEnd);
 }
 
-void TerrainSystem::SetTerrainAabb(const AZ::Aabb& worldBounds)
+void TerrainSystem::SetTerrainHeightBounds(const AzFramework::Terrain::FloatRange& heightRange)
 {   
-    m_requestedSettings.m_worldBounds = worldBounds;
+    m_requestedSettings.m_heightRange = heightRange;
     m_terrainSettingsDirty = true;
 }
 
@@ -177,7 +178,12 @@ void TerrainSystem::SetTerrainSurfaceDataQueryResolution(float queryResolution)
 
 AZ::Aabb TerrainSystem::GetTerrainAabb() const
 {
-    return m_currentSettings.m_worldBounds;
+    return ClampZBoundsToHeightBounds(m_cachedAreaBounds);
+}
+
+AzFramework::Terrain::FloatRange TerrainSystem::GetTerrainHeightBounds() const
+{
+    return m_currentSettings.m_heightRange;
 }
 
 float TerrainSystem::GetTerrainHeightQueryResolution() const
@@ -270,16 +276,26 @@ void TerrainSystem::InterpolateHeights(const AZStd::array<float,4>& heights, con
     outExists = exists[existsIndex];
 }
 
+void TerrainSystem::RecalculateCachedBounds()
+{
+    m_cachedAreaBounds = AZ::Aabb::CreateNull();
+    for (const auto& [entityid, area] : m_registeredAreas)
+    {
+        m_cachedAreaBounds.AddAabb(area.m_areaBounds);
+    }
+}
 
-bool TerrainSystem::InWorldBounds(float x, float y) const
+AZ::Aabb TerrainSystem::ClampZBoundsToHeightBounds(const AZ::Aabb& aabb) const
 {
-    const float zTestValue = m_currentSettings.m_worldBounds.GetMin().GetZ();
-    const AZ::Vector3 testValue{ x, y, zTestValue };
-    if (m_currentSettings.m_worldBounds.Contains(testValue))
+    if (!aabb.IsValid())
     {
-        return true;
+        return aabb; // Don't try to clamp invalid aabbs
     }
-    return false;
+    AZ::Vector3 min = aabb.GetMin();
+    AZ::Vector3 max = aabb.GetMax();
+    min.SetZ(AZ::GetClamp<float>(min.GetZ(), m_currentSettings.m_heightRange.m_min, m_currentSettings.m_heightRange.m_max));
+    max.SetZ(AZ::GetClamp<float>(max.GetZ(), m_currentSettings.m_heightRange.m_min, m_currentSettings.m_heightRange.m_max));
+    return AZ::Aabb::CreateFromMinMax(min, max);
 }
 
 // Generate positions to be queried based on the sampler type.
@@ -289,34 +305,21 @@ void TerrainSystem::GenerateQueryPositions(const AZStd::span<const AZ::Vector3>&
 {
     AZ_PROFILE_FUNCTION(Terrain);
 
-    const float minHeight = m_currentSettings.m_worldBounds.GetMin().GetZ();
+    const float minHeight = m_currentSettings.m_heightRange.m_min;
     for (auto& position : inPositions)
     {
         switch(sampler)
         {
         case AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR:
             {
-                if (InWorldBounds(position.GetX(), position.GetY()))
-                {
-                    AZ::Vector2 normalizedDelta;
-                    AZ::Vector2 pos0;
-                    ClampPosition(position.GetX(), position.GetY(), queryResolution, pos0, normalizedDelta);
-                    const AZ::Vector2 pos1(pos0.GetX() + queryResolution, pos0.GetY() + queryResolution);
-                    outPositions.emplace_back(AZ::Vector3(pos0.GetX(), pos0.GetY(), minHeight));
-                    outPositions.emplace_back(AZ::Vector3(pos1.GetX(), pos0.GetY(), minHeight));
-                    outPositions.emplace_back(AZ::Vector3(pos0.GetX(), pos1.GetY(), minHeight));
-                    outPositions.emplace_back(AZ::Vector3(pos1.GetX(), pos1.GetY(), minHeight));
-                }
-                else
-                {
-                    // If the query position isn't within the world bounds, we'll place that position 4x into the query list
-                    // instead of the normal bilinear positions, because we don't want to interpolate between partially inside and
-                    // partially outside. We just want to give it a min height and "terrain doesn't exist".
-                    outPositions.emplace_back(AZ::Vector3(position.GetX(), position.GetY(), minHeight));
-                    outPositions.emplace_back(AZ::Vector3(position.GetX(), position.GetY(), minHeight));
-                    outPositions.emplace_back(AZ::Vector3(position.GetX(), position.GetY(), minHeight));
-                    outPositions.emplace_back(AZ::Vector3(position.GetX(), position.GetY(), minHeight));
-                }
+                AZ::Vector2 normalizedDelta;
+                AZ::Vector2 pos0;
+                ClampPosition(position.GetX(), position.GetY(), queryResolution, pos0, normalizedDelta);
+                const AZ::Vector2 pos1(pos0.GetX() + queryResolution, pos0.GetY() + queryResolution);
+                outPositions.emplace_back(AZ::Vector3(pos0.GetX(), pos0.GetY(), minHeight));
+                outPositions.emplace_back(AZ::Vector3(pos1.GetX(), pos0.GetY(), minHeight));
+                outPositions.emplace_back(AZ::Vector3(pos0.GetX(), pos1.GetY(), minHeight));
+                outPositions.emplace_back(AZ::Vector3(pos1.GetX(), pos1.GetY(), minHeight));
             }
             break;
         case AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP:
@@ -520,19 +523,10 @@ void TerrainSystem::GetHeightsSynchronous(const AZStd::span<const AZ::Vector3>&
 float TerrainSystem::GetHeightSynchronous(float x, float y, Sampler sampler, bool* terrainExistsPtr) const
 {
     bool terrainExists = false;
-    float height = m_currentSettings.m_worldBounds.GetMin().GetZ();
-
-    if (!InWorldBounds(x, y))
-    {
-        if (terrainExistsPtr)
-        {
-            *terrainExistsPtr = terrainExists;
-        }
-        return height;
-    }
 
     AZStd::shared_lock<AZStd::shared_mutex> lock(m_areaMutex);
 
+    float height = m_currentSettings.m_heightRange.m_min;
     const float queryResolution = m_currentSettings.m_heightQueryResolution;
 
     switch (sampler)
@@ -583,12 +577,12 @@ float TerrainSystem::GetHeightSynchronous(float x, float y, Sampler sampler, boo
     }
 
     return AZ::GetClamp(
-        height, m_currentSettings.m_worldBounds.GetMin().GetZ(), m_currentSettings.m_worldBounds.GetMax().GetZ());
+        height, m_currentSettings.m_heightRange.m_min, m_currentSettings.m_heightRange.m_max);
 }
 
 float TerrainSystem::GetTerrainAreaHeight(float x, float y, bool& terrainExists) const
 {
-    const float worldMin = m_currentSettings.m_worldBounds.GetMin().GetZ();
+    const float worldMin = m_currentSettings.m_heightRange.m_min;
     AZ::Vector3 inPosition(x, y, worldMin);
     float height = worldMin;
     terrainExists = false;
@@ -695,20 +689,9 @@ void TerrainSystem::GetNormalsSynchronous(const AZStd::span<const AZ::Vector3>&
 
 AZ::Vector3 TerrainSystem::GetNormalSynchronous(float x, float y, Sampler sampler, bool* terrainExistsPtr) const
 {
-    AZStd::shared_lock<AZStd::shared_mutex> lock(m_areaMutex);
-
-    bool terrainExists = false;
-
     AZ::Vector3 outNormal = AZ::Vector3::CreateAxisZ();
 
-    if (!InWorldBounds(x, y))
-    {
-        if (terrainExistsPtr)
-        {
-            *terrainExistsPtr = terrainExists;
-            return outNormal;
-        }
-    }
+    AZStd::shared_lock<AZStd::shared_mutex> lock(m_areaMutex);
 
     const float range = m_currentSettings.m_heightQueryResolution / 2.0f;
     AZStd::array<AZ::Vector3, 4> directionVectors = { AZ::Vector3(x, y - range, 0.0f),
@@ -774,15 +757,6 @@ AzFramework::SurfaceData::SurfaceTagWeight TerrainSystem::GetMaxSurfaceWeightFro
 
     AzFramework::SurfaceData::SurfaceTagWeightList weightSet;
 
-    if (!InWorldBounds(x, y))
-    {
-        if (terrainExistsPtr)
-        {
-            *terrainExistsPtr = false;
-            return {};
-        }
-    }
-
     GetOrderedSurfaceWeights(x, y, sampler, weightSet, terrainExistsPtr);
 
     if (weightSet.empty())
@@ -1306,6 +1280,19 @@ void TerrainSystem::SubdivideRegionForJobs(
         subdivisionsY, maxNumJobs);
 }
 
+bool TerrainSystem::ContainedAabbTouchesEdge(const AZ::Aabb& outerAabb, const AZ::Aabb& innerAabb)
+{
+    return outerAabb.Contains(innerAabb) &&
+        (
+            outerAabb.GetMin().GetX() == innerAabb.GetMin().GetX() ||
+            outerAabb.GetMin().GetY() == innerAabb.GetMin().GetY() ||
+            outerAabb.GetMin().GetZ() == innerAabb.GetMin().GetZ() ||
+            outerAabb.GetMax().GetX() == innerAabb.GetMax().GetX() ||
+            outerAabb.GetMax().GetY() == innerAabb.GetMax().GetY() ||
+            outerAabb.GetMax().GetZ() == innerAabb.GetMax().GetZ()
+        );
+}
+
 void TerrainSystem::QueryRegion(
     const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
     TerrainDataMask requestedData,
@@ -1394,7 +1381,6 @@ void TerrainSystem::QueryRegionInternal(
     }
 }
 
-
 void TerrainSystem::RegisterArea(AZ::EntityId areaId)
 {
     AZStd::unique_lock<AZStd::shared_mutex> lock(m_areaMutex);
@@ -1409,6 +1395,7 @@ void TerrainSystem::RegisterArea(AZ::EntityId areaId)
     m_dirtyRegion.AddAabb(aabb);
     m_terrainHeightDirty = true;
     m_terrainSurfacesDirty = true;
+    m_cachedAreaBounds.AddAabb(aabb);
 }
 
 void TerrainSystem::UnregisterArea(AZ::EntityId areaId)
@@ -1428,6 +1415,12 @@ void TerrainSystem::UnregisterArea(AZ::EntityId areaId)
                 m_dirtyRegion.AddAabb(areaData.m_areaBounds);
                 m_terrainHeightDirty = true;
                 m_terrainSurfacesDirty = true;
+
+                if (ContainedAabbTouchesEdge(m_cachedAreaBounds, areaData.m_areaBounds))
+                {
+                    RecalculateCachedBounds();
+                }
+
                 return true;
             }
             return false;
@@ -1449,6 +1442,25 @@ void TerrainSystem::RefreshArea(AZ::EntityId areaId, AzFramework::Terrain::Terra
     expandedAabb.AddAabb(newAabb);
 
     RefreshRegion(expandedAabb, changeMask);
+
+    // Check to see which axis the aabbs changed in
+    bool xDiff = oldAabb.GetMin().GetX() != newAabb.GetMin().GetX() || oldAabb.GetMax().GetX() != newAabb.GetMax().GetX();
+    bool yDiff = oldAabb.GetMin().GetY() != newAabb.GetMin().GetY() || oldAabb.GetMax().GetY() != newAabb.GetMax().GetY();
+    bool zDiff = oldAabb.GetMin().GetZ() != newAabb.GetMin().GetZ() || oldAabb.GetMax().GetZ() != newAabb.GetMax().GetZ();
+
+    if ((xDiff && (m_cachedAreaBounds.GetMin().GetX() == oldAabb.GetMin().GetX() || m_cachedAreaBounds.GetMax().GetX() == oldAabb.GetMax().GetX())) ||
+        (yDiff && (m_cachedAreaBounds.GetMin().GetY() == oldAabb.GetMin().GetY() || m_cachedAreaBounds.GetMax().GetY() == oldAabb.GetMax().GetY())) ||
+        (zDiff && (m_cachedAreaBounds.GetMin().GetZ() == oldAabb.GetMin().GetZ() || m_cachedAreaBounds.GetMax().GetZ() == oldAabb.GetMax().GetZ())))
+    {
+        // Old aabb is on the edge of the bounds in at least one axis, and moved on that axis, so it will require a full refresh
+        RecalculateCachedBounds();
+    }
+    else if(!m_cachedAreaBounds.Contains(newAabb))
+    {
+        // Old Aabb was inside the bounds and new aabb is outside the bounds, so just add it.
+        m_cachedAreaBounds.AddAabb(newAabb);
+    }
+
 }
 
 void TerrainSystem::RefreshRegion(
@@ -1475,27 +1487,30 @@ void TerrainSystem::OnTick(float /*deltaTime*/, AZ::ScriptTimePoint /*time*/)
     {
         terrainSettingsChanged = true;
         m_terrainSettingsDirty = false;
+        if (m_currentSettings.m_heightRange.IsValid())
+        {
+            m_dirtyRegion = ClampZBoundsToHeightBounds(m_cachedAreaBounds);
+        }
 
         // This needs to happen before the "system active" check below, because activating the system will cause the various
         // terrain layer areas to request the current world bounds.
-        if (m_requestedSettings.m_worldBounds != m_currentSettings.m_worldBounds)
+        if (m_requestedSettings.m_heightRange != m_requestedSettings.m_heightRange)
         {
-            m_dirtyRegion = m_currentSettings.m_worldBounds;
-            m_dirtyRegion.AddAabb(m_requestedSettings.m_worldBounds);
             m_terrainHeightDirty = true;
             m_terrainSurfacesDirty = true;
-            m_currentSettings.m_worldBounds = m_requestedSettings.m_worldBounds;
+            m_currentSettings.m_heightRange = m_requestedSettings.m_heightRange;
+
+            // Add the cached area bounds clamped to the new range, so both the old and new range are included.
+            m_dirtyRegion.AddAabb(ClampZBoundsToHeightBounds(m_cachedAreaBounds));
         }
 
         if (m_requestedSettings.m_heightQueryResolution != m_currentSettings.m_heightQueryResolution)
         {
-            m_dirtyRegion.AddAabb(m_requestedSettings.m_worldBounds);
             m_terrainHeightDirty = true;
         }
 
         if (m_requestedSettings.m_surfaceDataQueryResolution != m_currentSettings.m_surfaceDataQueryResolution)
         {
-            m_dirtyRegion.AddAabb(m_requestedSettings.m_worldBounds);
             m_terrainSurfacesDirty = true;
         }
 
@@ -1522,7 +1537,7 @@ void TerrainSystem::OnTick(float /*deltaTime*/, AZ::ScriptTimePoint /*time*/)
 
         // Make sure to set these *before* calling OnTerrainDataChanged, since it's possible that subsystems reacting to that call will
         // cause the data to become dirty again.
-        AZ::Aabb dirtyRegion = m_dirtyRegion;
+        AZ::Aabb dirtyRegion = ClampZBoundsToHeightBounds(m_dirtyRegion);
         m_terrainHeightDirty = false;
         m_terrainSurfacesDirty = false;
         m_dirtyRegion = AZ::Aabb::CreateNull();

+ 11 - 4
Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h

@@ -64,8 +64,10 @@ namespace Terrain
         float GetTerrainSurfaceDataQueryResolution() const override;
         void SetTerrainSurfaceDataQueryResolution(float queryResolution) override;
 
-        AZ::Aabb GetTerrainAabb() const override;
-        void SetTerrainAabb(const AZ::Aabb& worldBounds) override;
+        virtual AZ::Aabb GetTerrainAabb() const override;
+
+        AzFramework::Terrain::FloatRange GetTerrainHeightBounds() const override;
+        void SetTerrainHeightBounds(const AzFramework::Terrain::FloatRange& heightRange) override;
 
         bool TerrainAreaExistsInBounds(const AZ::Aabb& bounds) const override;
 
@@ -198,6 +200,8 @@ namespace Terrain
             int32_t numSamplesX, int32_t numSamplesY, int32_t maxNumJobs, int32_t minPointsPerJob,
             int32_t& subdivisionsX, int32_t& subdivisionsY);
 
+        static bool ContainedAabbTouchesEdge(const AZ::Aabb& outerAabb, const AZ::Aabb& innerAabb);
+
         //! 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(
@@ -219,7 +223,6 @@ namespace Terrain
         static void RoundPosition(float x, float y, float queryResolution, AZ::Vector2& outPosition);
         static void InterpolateHeights(const AZStd::array<float, 4>& heights, const AZStd::array<bool, 4>& exists,
             float lerpX, float lerpY, float& outHeight, bool& outExists);
-        bool InWorldBounds(float x, float y) const;
 
         AZ::EntityId FindBestAreaEntityAtPosition(const AZ::Vector3& position, AZ::Aabb& bounds) const;
         void GetOrderedSurfaceWeights(
@@ -268,9 +271,12 @@ namespace Terrain
         // AZ::TickBus::Handler overrides ...
         void OnTick(float deltaTime, AZ::ScriptTimePoint time) override;
 
+        void RecalculateCachedBounds();
+        AZ::Aabb ClampZBoundsToHeightBounds(const AZ::Aabb& aabb) const;
+
         struct TerrainSystemSettings
         {
-            AZ::Aabb m_worldBounds;
+            AzFramework::Terrain::FloatRange m_heightRange;
             float m_heightQueryResolution{ 1.0f };
             float m_surfaceDataQueryResolution{ 1.0f };
             bool m_systemActive{ false };
@@ -283,6 +289,7 @@ namespace Terrain
         bool m_terrainHeightDirty = false;
         bool m_terrainSurfacesDirty = false;
         AZ::Aabb m_dirtyRegion;
+        AZ::Aabb m_cachedAreaBounds;
 
         // Cached data for each terrain area to use when looking up terrain data.
         struct TerrainAreaData

+ 4 - 4
Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp

@@ -184,8 +184,8 @@ TEST_F(LayerSpawnerComponentTest, LayerSpawnerCreatesGroundPlaneWhenUseGroundPla
 {
     // Create a terrain world with height bounds from -128 to 128.
     const float queryResolution = 1.0f;
-    const AZ::Aabb worldBounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-128.0f), AZ::Vector3(128.0f));
-    auto terrainSystem = CreateAndActivateTerrainSystem(queryResolution, worldBounds);
+    const AzFramework::Terrain::FloatRange heightBounds = { -128.0f, 128.0f };
+    auto terrainSystem = CreateAndActivateTerrainSystem(queryResolution, heightBounds);
 
     // Create a terrain spawner with useGroundPlane enabled and a box from 0 to 32.
     Terrain::TerrainLayerSpawnerConfig config;
@@ -225,8 +225,8 @@ TEST_F(LayerSpawnerComponentTest, LayerSpawnerDoesNotCreateGroundPlaneWhenUseGro
 {
     // Create a terrain world with height bounds from -128 to 128.
     const float queryResolution = 1.0f;
-    const AZ::Aabb worldBounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-128.0f), AZ::Vector3(128.0f));
-    auto terrainSystem = CreateAndActivateTerrainSystem(queryResolution, worldBounds);
+    const AzFramework::Terrain::FloatRange heightBounds = { -128.0f, 128.0f };
+    auto terrainSystem = CreateAndActivateTerrainSystem(queryResolution, heightBounds);
 
     // Create a terrain spawner with useGroundPlane disabled and a box from 0 to 32.
     Terrain::TerrainLayerSpawnerConfig config;

+ 1 - 5
Gems/Terrain/Code/Tests/TerrainHeightGradientListTests.cpp

@@ -113,10 +113,9 @@ TEST_F(TerrainHeightGradientListComponentTest, TerrainHeightGradientListReturnsH
     ON_CALL(mockShapeRequests, GetEncompassingAabb).WillByDefault(Return(aabb));
 
     const float worldMax = 10000.0f;
-    const AZ::Aabb worldAabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3(min), AZ::Vector3(worldMax));
     NiceMock<UnitTest::MockTerrainDataRequests> mockterrainDataRequests;
     ON_CALL(mockterrainDataRequests, GetTerrainHeightQueryResolution).WillByDefault(Return(1.0f));
-    ON_CALL(mockterrainDataRequests, GetTerrainAabb).WillByDefault(Return(worldAabb));
+    ON_CALL(mockterrainDataRequests, GetTerrainHeightBounds).WillByDefault(Return(AzFramework::Terrain::FloatRange({0.0f, worldMax})));
 
     ActivateEntity(entity.get());
 
@@ -161,11 +160,8 @@ TEST_F(TerrainHeightGradientListComponentTest, TerrainHeightGradientListGetHeigh
     NiceMock<UnitTest::MockShapeComponentRequests> mockShapeRequests(entity->GetId());
     ON_CALL(mockShapeRequests, GetEncompassingAabb).WillByDefault(Return(aabb));
 
-    const float worldMax = 10000.0f;
-    const AZ::Aabb worldAabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3(min), AZ::Vector3(worldMax));
     NiceMock<UnitTest::MockTerrainDataRequests> mockterrainDataRequests;
     ON_CALL(mockterrainDataRequests, GetTerrainHeightQueryResolution).WillByDefault(Return(1.0f));
-    ON_CALL(mockterrainDataRequests, GetTerrainAabb).WillByDefault(Return(worldAabb));
 
     ActivateEntity(entity.get());
 

+ 8 - 6
Gems/Terrain/Code/Tests/TerrainSystemSettingsTests.cpp

@@ -146,8 +146,8 @@ namespace UnitTest
 
         // Create and activate the terrain system with world height min/max of 5 and 15.
         const float queryResolution = 1.0f;
-        const AZ::Aabb terrainWorldBounds = AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 5.0f, 20.0f, 20.0f, 15.0f);
-        auto terrainSystem = CreateAndActivateTerrainSystem(queryResolution, terrainWorldBounds);
+        AzFramework::Terrain::FloatRange heightBounds = { 5.0f, 15.0f };
+        auto terrainSystem = CreateAndActivateTerrainSystem(queryResolution, heightBounds);
 
         // Test a set of points from (0,0) - (20,20). If the world min/max clamp is working, we should always get 5 <= height <= 15.
         for (float x = 0.0f; x <= 20.0f; x += queryResolution)
@@ -158,8 +158,8 @@ namespace UnitTest
                 terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::DEFAULT, &heightQueryTerrainExists);
 
             // Verify all the heights are between 5 and 15.
-            EXPECT_GE(height, terrainWorldBounds.GetMin().GetZ());
-            EXPECT_LE(height, terrainWorldBounds.GetMax().GetZ());
+            EXPECT_GE(height, heightBounds.m_min);
+            EXPECT_LE(height, heightBounds.m_max);
         }
     }
 
@@ -181,7 +181,8 @@ namespace UnitTest
 
         // Create and activate the terrain system with a world bounds that matches the spawner box, and a query resolution of 10.
         const float queryResolution = 10.0f;
-        auto terrainSystem = CreateAndActivateTerrainSystem(queryResolution, spawnerBox);
+        AzFramework::Terrain::FloatRange heightBounds = { spawnerBox.GetMin().GetZ(), spawnerBox.GetMax().GetZ() };
+        auto terrainSystem = CreateAndActivateTerrainSystem(queryResolution, heightBounds);
 
         for (auto sampler : { AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR,
                               AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP,
@@ -259,7 +260,8 @@ namespace UnitTest
         // Create and activate the terrain system with a world bounds that matches the spawner box, and a query resolution of 10.
         const float heightQueryResolution = 1.0f;
         const float surfaceQueryResolution = 10.0f;
-        auto terrainSystem = CreateAndActivateTerrainSystem(heightQueryResolution, surfaceQueryResolution, spawnerBox);
+        AzFramework::Terrain::FloatRange heightBounds = { spawnerBox.GetMin().GetZ(), spawnerBox.GetMax().GetZ() };
+        auto terrainSystem = CreateAndActivateTerrainSystem(heightQueryResolution, surfaceQueryResolution, heightBounds);
 
         for (auto sampler : { AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR,
                               AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP,

+ 7 - 2
Gems/Terrain/Code/Tests/TerrainSystemTest.cpp

@@ -238,7 +238,12 @@ namespace UnitTest
         // Create and activate the terrain system with our testing defaults for world bounds and query resolution.
         auto terrainSystem = CreateAndActivateTerrainSystem();
 
-        AZ::Aabb worldBounds = terrainSystem->GetTerrainAabb();
+        AzFramework::Terrain::FloatRange heightBounds;
+        AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
+            heightBounds, &AzFramework::Terrain::TerrainDataRequestBus::Events::GetTerrainHeightBounds);
+
+        // Create an arbitrary world bounds to test since the bounds of the terrain system will be 0 with no terrain areas.
+        AZ::Aabb worldBounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-10.0f, -10.0f, -10.0f), AZ::Vector3(10.0f, 10.0f, 10.0f));
 
         // Loop through several points within the world bounds, including on the edges, and verify that they all return false for
         // terrainExists with default heights and normals.
@@ -251,7 +256,7 @@ namespace UnitTest
                 float height =
                     terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists);
                 EXPECT_FALSE(terrainExists);
-                EXPECT_FLOAT_EQ(height, worldBounds.GetMin().GetZ());
+                EXPECT_FLOAT_EQ(height, heightBounds.m_min);
 
                 terrainExists = true;
                 AZ::Vector3 normal =

+ 8 - 6
Gems/Terrain/Code/Tests/TerrainTestFixtures.cpp

@@ -192,20 +192,20 @@ namespace UnitTest
     // Create a terrain system with reasonable defaults for testing, but with the ability to override the defaults
     // on a test-by-test basis.
     AZStd::unique_ptr<Terrain::TerrainSystem> TerrainBaseFixture::CreateAndActivateTerrainSystem(
-        float queryResolution, AZ::Aabb worldBounds) const
+        float queryResolution, AzFramework::Terrain::FloatRange heightBounds) const
     {
         const float defaultSurfaceQueryResolution = 1.0f;
-        return CreateAndActivateTerrainSystem(queryResolution, defaultSurfaceQueryResolution, worldBounds);
+        return CreateAndActivateTerrainSystem(queryResolution, defaultSurfaceQueryResolution, heightBounds);
     }
 
     // Create a terrain system with reasonable defaults for testing, but with the ability to override the defaults
     // on a test-by-test basis.
     AZStd::unique_ptr<Terrain::TerrainSystem> TerrainBaseFixture::CreateAndActivateTerrainSystem(
-        float heightQueryResolution, float surfaceQueryResolution, AZ::Aabb worldBounds) const
+        float heightQueryResolution, float surfaceQueryResolution, const AzFramework::Terrain::FloatRange& heightBounds) const
     {
         // Create the terrain system and give it one tick to fully initialize itself.
         auto terrainSystem = AZStd::make_unique<Terrain::TerrainSystem>();
-        terrainSystem->SetTerrainAabb(worldBounds);
+        terrainSystem->SetTerrainHeightBounds(heightBounds);
         terrainSystem->SetTerrainHeightQueryResolution(heightQueryResolution);
         terrainSystem->SetTerrainSurfaceDataQueryResolution(surfaceQueryResolution);
         terrainSystem->Activate();
@@ -243,7 +243,8 @@ namespace UnitTest
 
         // Create the terrain system (do this after creating the terrain layer entity to ensure that we don't need any data refreshes)
         // Also ensure to do this after creating the global JobManager.
-        m_terrainSystem = CreateAndActivateTerrainSystem(queryResolution, worldBounds);
+        AzFramework::Terrain::FloatRange heightBounds = { worldBounds.GetMin().GetZ(), worldBounds.GetMax().GetZ() };
+        m_terrainSystem = CreateAndActivateTerrainSystem(queryResolution, heightBounds);
     }
 
     void TerrainBaseFixture::DestroyTestTerrainSystem()
@@ -388,7 +389,8 @@ namespace UnitTest
 
         // Create the terrain system (do this after creating the terrain layer entity to ensure that we don't need any data refreshes)
         // Also ensure to do this after creating the global JobManager.
-        m_terrainSystem = CreateAndActivateTerrainSystem(queryResolution, worldBounds);
+        AzFramework::Terrain::FloatRange heightBounds = { worldBounds.GetMin().GetZ(), worldBounds.GetMax().GetZ() };
+        m_terrainSystem = CreateAndActivateTerrainSystem(queryResolution, heightBounds);
     }
 
     TerrainSystemTestFixture::TerrainSystemTestFixture()

+ 2 - 2
Gems/Terrain/Code/Tests/TerrainTestFixtures.h

@@ -90,9 +90,9 @@ namespace UnitTest
         // on a test-by-test basis.
         AZStd::unique_ptr<Terrain::TerrainSystem> CreateAndActivateTerrainSystem(
             float queryResolution = 1.0f,
-            AZ::Aabb worldBounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-128.0f), AZ::Vector3(128.0f))) const;
+            AzFramework::Terrain::FloatRange heightBounds = {-128.0f, 128.0f}) const;
         AZStd::unique_ptr<Terrain::TerrainSystem> CreateAndActivateTerrainSystem(
-            float heightQueryResolution, float surfaceQueryResolution, AZ::Aabb worldBounds) const;
+            float heightQueryResolution, float surfaceQueryResolution, const AzFramework::Terrain::FloatRange& heightBounds) const;
 
         void CreateTestTerrainSystem(const AZ::Aabb& worldBounds, float queryResolution, uint32_t numSurfaces);
         void CreateTestTerrainSystemWithSurfaceGradients(const AZ::Aabb& worldBounds, float queryResolution);

+ 11 - 5
Gems/Terrain/Code/Tests/TerrainWorldComponentTests.cpp

@@ -60,20 +60,26 @@ TEST_F(TerrainWorldComponentTest, ComponentCreatesAndActivatesTerrainSystem)
 TEST_F(TerrainWorldComponentTest, WorldMinAndMaxAffectTerrainSystem)
 {
     // Verify that the Z component of the Terrain World Component's World Min and World Max set the Terrain System's min/max.
-    // (We aren't testing the XY components because those will eventually get removed)
+    // The z min/max should be returned with GetTerrainHeightBounds, and since there are no terrain areas, the aabb returned
+    // from worldBounds should be invalid.
 
     Terrain::TerrainWorldConfig config;
-    config.m_worldMin = AZ::Vector3(0.0f, 0.0f, -345.0f);
-    config.m_worldMax = AZ::Vector3(1024.0f, 1024.0f, 678.0f);
+    config.m_minHeight = -345.0f;
+    config.m_maxHeight = 678.0f;
 
     auto entity = CreateAndActivateTerrainWorldComponent(config);
 
+    AzFramework::Terrain::FloatRange heightBounds;
+    AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
+        heightBounds, &AzFramework::Terrain::TerrainDataRequestBus::Events::GetTerrainHeightBounds);
+
     AZ::Aabb worldBounds = AZ::Aabb::CreateNull();
     AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
         worldBounds, &AzFramework::Terrain::TerrainDataRequestBus::Events::GetTerrainAabb);
 
-    EXPECT_NEAR(config.m_worldMin.GetZ(), worldBounds.GetMin().GetZ(), 0.001f);
-    EXPECT_NEAR(config.m_worldMax.GetZ(), worldBounds.GetMax().GetZ(), 0.001f);
+    EXPECT_NEAR(config.m_minHeight, heightBounds.m_min, 0.001f);
+    EXPECT_NEAR(config.m_maxHeight, heightBounds.m_max, 0.001f);
+    EXPECT_FALSE(worldBounds.IsValid());
 
     entity.reset();
 }