Bladeren bron

Terrain Feature Processor separated into several classes. Macro materials abstracted from meshes. (#6350)

* Breaking up terrain FP wip - macro materials decoupled from meshes. Mesh creation and rendering pulled out from MeshFeatureProcessor into TerrainMeshManager. Stubs added for macro and detail material managers.

Signed-off-by: Ken Pruiksma <[email protected]>

* Separated macro material management from the terrain feature processor. Also separated bindless image array handling from terrain feature processor.

Signed-off-by: Ken Pruiksma <[email protected]>

* Detail materials separated from terrain feature processor. Also pulled out Aabb2i and Vector2i into their own simple classes

Signed-off-by: Ken Pruiksma <[email protected]>

* Changed some classes so that when the SRG changes the classes don't need to be completely reinitialized and can instead just update their indices and push data to the new srg. Fixed an issue where MacroMaterialData wasn't being exposed to ScriptCanvas. Terrain shader reloads should now work correctly. Also added some debug logging to help catch an issue where sometimes detail materials don't seem to load.

Signed-off-by: Ken Pruiksma <[email protected]>

* Terrain PR reveiw updates

Signed-off-by: Ken Pruiksma <[email protected]>

* Some small PR review fixes. More comments in TerrainDetailMaterialManager.h

Signed-off-by: Ken Pruiksma <[email protected]>

* Fixing unused variable causing clang failure

Signed-off-by: Ken Pruiksma <[email protected]>

* Fixing unused variable in release.

Signed-off-by: Ken Pruiksma <[email protected]>

* Fixing a forward declare that oddly didn't work on linux.

Signed-off-by: Ken Pruiksma <[email protected]>

* Fixing linux missing include... not sure why only linux failed on this.

Signed-off-by: Ken Pruiksma <[email protected]>

* Adding missing include

Signed-off-by: Ken Pruiksma <[email protected]>
Ken Pruiksma 3 jaren geleden
bovenliggende
commit
2f1346c719
29 gewijzigde bestanden met toevoegingen van 2965 en 1875 verwijderingen
  1. 1 0
      Gems/Atom/Feature/Common/Assets/ShaderResourceGroups/SceneSrgAll.azsli
  2. 0 1
      Gems/Atom/Feature/Common/Assets/ShaderResourceGroups/ViewSrgAll.azsli
  3. 3 3
      Gems/Atom/Feature/Common/Code/Source/Utils/GpuBufferHandler.cpp
  4. 1 1
      Gems/Terrain/Assets/Materials/Terrain/DefaultPbrTerrain.material
  5. 1 1
      Gems/Terrain/Assets/Materials/Terrain/PbrTerrain.materialtype
  6. 35 0
      Gems/Terrain/Assets/Shaders/Terrain/SceneSrg.azsli
  7. 122 115
      Gems/Terrain/Assets/Shaders/Terrain/TerrainCommon.azsli
  8. 7 7
      Gems/Terrain/Assets/Shaders/Terrain/TerrainDetailHelpers.azsli
  9. 69 48
      Gems/Terrain/Assets/Shaders/Terrain/TerrainPBR_ForwardPass.azsl
  10. 27 1
      Gems/Terrain/Assets/Shaders/Terrain/TerrainSrg.azsli
  11. 25 3
      Gems/Terrain/Assets/Shaders/Terrain/Terrain_DepthPass.azsl
  12. 0 80
      Gems/Terrain/Assets/Shaders/Terrain/ViewSrg.azsli
  13. 44 0
      Gems/Terrain/Code/Source/TerrainRenderer/Aabb2i.cpp
  14. 34 0
      Gems/Terrain/Code/Source/TerrainRenderer/Aabb2i.h
  15. 101 0
      Gems/Terrain/Code/Source/TerrainRenderer/BindlessImageArrayHandler.cpp
  16. 48 0
      Gems/Terrain/Code/Source/TerrainRenderer/BindlessImageArrayHandler.h
  17. 912 0
      Gems/Terrain/Code/Source/TerrainRenderer/TerrainDetailMaterialManager.cpp
  18. 226 0
      Gems/Terrain/Code/Source/TerrainRenderer/TerrainDetailMaterialManager.h
  19. 176 1328
      Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.cpp
  20. 29 285
      Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.h
  21. 19 0
      Gems/Terrain/Code/Source/TerrainRenderer/TerrainMacroMaterialBus.cpp
  22. 4 2
      Gems/Terrain/Code/Source/TerrainRenderer/TerrainMacroMaterialBus.h
  23. 412 0
      Gems/Terrain/Code/Source/TerrainRenderer/TerrainMacroMaterialManager.cpp
  24. 116 0
      Gems/Terrain/Code/Source/TerrainRenderer/TerrainMacroMaterialManager.h
  25. 354 0
      Gems/Terrain/Code/Source/TerrainRenderer/TerrainMeshManager.cpp
  26. 117 0
      Gems/Terrain/Code/Source/TerrainRenderer/TerrainMeshManager.h
  27. 41 0
      Gems/Terrain/Code/Source/TerrainRenderer/Vector2i.cpp
  28. 29 0
      Gems/Terrain/Code/Source/TerrainRenderer/Vector2i.h
  29. 12 0
      Gems/Terrain/Code/terrain_files.cmake

+ 1 - 0
Gems/Atom/Feature/Common/Assets/ShaderResourceGroups/SceneSrgAll.azsli

@@ -13,4 +13,5 @@
 #ifdef AZ_COLLECTING_PARTIAL_SRGS
 #include <Atom/RPI/ShaderResourceGroups/DefaultSceneSrg.azsli>
 #include <Atom/Feature/Common/Assets/ShaderResourceGroups/SceneSrg.azsli>
+#include <Terrain/Assets/Shaders/Terrain/SceneSrg.azsli> // Temporary until gem partial view srgs can be included automatically.
 #endif

+ 0 - 1
Gems/Atom/Feature/Common/Assets/ShaderResourceGroups/ViewSrgAll.azsli

@@ -12,5 +12,4 @@
 
 #ifdef AZ_COLLECTING_PARTIAL_SRGS
 #include <Atom/Feature/Common/Assets/ShaderResourceGroups/ViewSrg.azsli>
-#include <Terrain/Assets/Shaders/Terrain/ViewSrg.azsli> // Temporary until gem partial view srgs can be included automatically.
 #endif

+ 3 - 3
Gems/Atom/Feature/Common/Code/Source/Utils/GpuBufferHandler.cpp

@@ -24,14 +24,14 @@ namespace AZ
         {
             m_elementSize = descriptor.m_elementSize;
             m_elementCount = 0;
-
+            
             m_bufferIndex = descriptor.m_srgLayout->FindShaderInputBufferIndex(Name(descriptor.m_bufferSrgName));
-            AZ_Error(ClassName, m_bufferIndex.IsValid(), "Unable to find %s in view shader resource group.", descriptor.m_bufferSrgName.c_str());
+            AZ_Error(ClassName, m_bufferIndex.IsValid(), "Unable to find %s in %s shader resource group.", descriptor.m_bufferSrgName.c_str(), descriptor.m_srgLayout->GetName().GetCStr());
 
             if (!descriptor.m_elementCountSrgName.empty())
             {
                 m_elementCountIndex = descriptor.m_srgLayout->FindShaderInputConstantIndex(Name(descriptor.m_elementCountSrgName));
-                AZ_Error(ClassName, m_elementCountIndex.IsValid(), "Unable to find %s in view shader resource group.", descriptor.m_elementCountSrgName.c_str());
+                AZ_Error(ClassName, m_elementCountIndex.IsValid(), "Unable to find %s in %s shader resource group.", descriptor.m_elementCountSrgName.c_str(), descriptor.m_srgLayout->GetName().GetCStr());
             }
 
             if (m_bufferIndex.IsValid())

+ 1 - 1
Gems/Terrain/Assets/Materials/Terrain/DefaultPbrTerrain.material

@@ -5,7 +5,7 @@
     "propertyLayoutVersion": 1,
     "properties": {
         "baseColor": {
-            "color": [ 0.18, 0.18, 0.18 ]
+            "color": [ 0.18, 0.18, 0.18 ] 
         }
     }
 }

+ 1 - 1
Gems/Terrain/Assets/Materials/Terrain/PbrTerrain.materialtype

@@ -107,7 +107,7 @@
                     "displayName": "Detail Texture UV Multiplier",
                     "description": "How many times to repeat the detail texture per sector",
                     "type": "Float",
-                    "defaultValue": 8.0,
+                    "defaultValue": 0.5,
                     "connection": {
                         "type": "ShaderInput",
                         "id": "m_detailTextureMultiplier"

+ 35 - 0
Gems/Terrain/Assets/Shaders/Terrain/SceneSrg.azsli

@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#ifndef AZ_COLLECTING_PARTIAL_SRGS
+#error Do not include this file directly. Include the main .srgi file instead.
+#endif
+
+partial ShaderResourceGroup SceneSrg
+{
+    Sampler HeightmapSampler
+    {
+        MinFilter = Linear;
+        MagFilter = Linear;
+        MipFilter = Point;
+        AddressU = Clamp;
+        AddressV = Clamp;
+        AddressW = Clamp;
+    };
+
+    struct TerrainWorldData
+    {
+        float3 m_min;
+        float m_padding1;
+        float3 m_max;
+        float m_padding2;
+    };
+
+    Texture2D m_heightmapImage;
+    TerrainWorldData m_terrainWorldData;
+}

+ 122 - 115
Gems/Terrain/Assets/Shaders/Terrain/TerrainCommon.azsli

@@ -15,34 +15,13 @@
 
 ShaderResourceGroup ObjectSrg : SRG_PerObject
 {
-    struct TerrainData
+    struct PatchData
     {
-        float2 m_uvMin;
-        float2 m_uvMax;
-        float2 m_uvStep;
-        float m_sampleSpacing;
-        float m_heightScale;
+        float2 m_xyTranslation;
+        float m_xyScale;
     };
 
-    struct MacroMaterialData
-    {
-        float2 m_uvMin;
-        float2 m_uvMax;
-        float m_normalFactor;
-        bool m_flipNormalX;
-        bool m_flipNormalY;
-        uint m_mapsInUse;
-    };
-
-    row_major float3x4 m_modelToWorld;
-
-    TerrainData m_terrainData;
-
-    MacroMaterialData m_macroMaterialData[4];
-    uint m_macroMaterialCount;
-
-    Texture2D m_macroColorMap[4];
-    Texture2D m_macroNormalMap[4];
+    PatchData m_patchData;
 
     // The below shouldn't be in this SRG but needs to be for now because the lighting functions depend on them.
 
@@ -117,109 +96,137 @@ option bool o_useTerrainSmoothing = false;
 struct VertexInput
 {
     float2 m_position : POSITION;
-    float2 m_uv : UV;
 };
 
-// Sample a texture with a 5 tap B-Spline. Consider ripping this out and putting in a more general location.
-// This function samples a 4x4 neighborhood around the uv. Normally this would take 16 samples, but by taking
-// advantage of bilinear filtering this can be done with 9 taps on the edges between pixels. The cost is further
-// reduced by dropping the diagonals.
-float SampleBSpline5Tap(Texture2D texture, SamplerState textureSampler, float2 uv, float2 textureSize, float2 rcpTextureSize)
+// This class is used to calculate heights and normals for terrain. Using a class for this was the easiest way to
+// de-duplicate code between the forward and depth shaders.
+class HeightContext
 {
-    // Think of sample locations in the 4x4 neighborhood as having a top left coordinate of 0,0 and
-    // a bottom right coordinate of 3,3.
-
-    // Find the position in texture space then round it to get the center of the 1,1 pixel (tc1)
-    float2 texelPos = uv * textureSize;
-    float2 tc1= floor(texelPos - 0.5) + 0.5;
-
-    // Offset from center position to texel
-    float2 f = texelPos - tc1;
-
-    // Compute B-Spline weights based on the offset
-    float2 OneMinusF = (1.0 - f);
-    float2 OneMinusF2 = OneMinusF * OneMinusF;
-    float2 OneMinusF3 = OneMinusF2 * OneMinusF;
-    float2 w0 = OneMinusF3;
-    float2 w1 = 4.0 + 3.0 * f * f * f - 6.0 * f * f;
-    float2 w2 = 4.0 + 3.0 * OneMinusF3 - 6.0 * OneMinusF2;
-    float2 w3 = f * f * f;
-
-    float2 w12 = w1 + w2;
-
-    // Compute uv coordinates for sampling the texture
-    float2 tc0 = (tc1 - 1.0f) * rcpTextureSize;
-    float2 tc3 = (tc1 + 2.0f) * rcpTextureSize;
-    float2 tc12 = (tc1 + w2 / w12) * rcpTextureSize;
-
-    // Compute sample weights
-    float sw0 = w12.x * w12.y; // middle
-    float sw1 = w12.x * w0.y;  // top
-    float sw2 = w0.x  * w12.y; // left
-    float sw3 = w12.x * w3.y;  // bottom
-    float sw4 = w3.x  * w12.y; // right
-
-    // total weight of samples to normalize result.
-    float totalWeight = sw0 + sw1 + sw2 + sw3 + sw4;
-
-    float result = 0.0f;
-    result += texture.SampleLevel(textureSampler, float2(tc12.x, tc12.y), 0.0).r * sw0;
-    result += texture.SampleLevel(textureSampler, float2(tc12.x,  tc0.y), 0.0).r * sw1;
-    result += texture.SampleLevel(textureSampler, float2( tc0.x, tc12.y), 0.0).r * sw2;
-    result += texture.SampleLevel(textureSampler, float2(tc12.x,  tc3.y), 0.0).r * sw3;
-    result += texture.SampleLevel(textureSampler, float2( tc3.x, tc12.y), 0.0).r * sw4;
-
-    return result / totalWeight;
-}
+    float3 m_worldMin;
+    float3 m_worldMax;
+    float2 m_xyPosition;
 
-float4x4 GetObject_WorldMatrix()
-{
-    float4x4 modelToWorld = float4x4(
-        float4(1, 0, 0, 0),
-        float4(0, 1, 0, 0),
-        float4(0, 0, 1, 0),
-        float4(0, 0, 0, 1));
-
-    modelToWorld[0] = ObjectSrg::m_modelToWorld[0];
-    modelToWorld[1] = ObjectSrg::m_modelToWorld[1];
-    modelToWorld[2] = ObjectSrg::m_modelToWorld[2];
-    return modelToWorld;
-}
+    float2 m_textureSize;
+    float2 m_rcpTextureSize;
+    float2 m_sampleSpacing;
+    float2 m_rcpSampleSpacing;
 
-float GetHeight(float2 origUv)
-{
-    float2 halfStep = ObjectSrg::m_terrainData.m_uvStep * 0.5;
-    float2 uv = origUv * (1.0 - ObjectSrg::m_terrainData.m_uvStep) + halfStep;
+    float m_heightScale;
+    int2 m_heightmapCoord;
 
-    float height = 0.0f;
-    if (o_useTerrainSmoothing)
+    
+    // Sample a texture with a 5 tap B-Spline. Consider ripping this out and putting in a more general location.
+    // This function samples a 4x4 neighborhood around the uv. Normally this would take 16 samples, but by taking
+    // advantage of bilinear filtering this can be done with 9 taps on the edges between pixels. The cost is further
+    // reduced by dropping the diagonals.
+    float SampleBSpline5Tap(Texture2D texture, SamplerState textureSampler, float2 uv, float2 textureSize, float2 rcpTextureSize)
     {
-        float2 textureSize;
-        ViewSrg::m_heightmapImage.GetDimensions(textureSize.x, textureSize.y);
-        height = SampleBSpline5Tap(ViewSrg::m_heightmapImage, ViewSrg::HeightmapSampler, uv, textureSize, rcp(textureSize));
+        // Think of sample locations in the 4x4 neighborhood as having a top left coordinate of 0,0 and
+        // a bottom right coordinate of 3,3.
+
+        // Find the position in texture space then round it to get the center of the 1,1 pixel (tc1)
+        float2 texelPos = uv * textureSize;
+        float2 tc1= floor(texelPos - 0.5) + 0.5;
+
+        // Offset from center position to texel
+        float2 f = texelPos - tc1;
+
+        // Compute B-Spline weights based on the offset
+        float2 OneMinusF = (1.0 - f);
+        float2 OneMinusF2 = OneMinusF * OneMinusF;
+        float2 OneMinusF3 = OneMinusF2 * OneMinusF;
+        float2 w0 = OneMinusF3;
+        float2 w1 = 4.0 + 3.0 * f * f * f - 6.0 * f * f;
+        float2 w2 = 4.0 + 3.0 * OneMinusF3 - 6.0 * OneMinusF2;
+        float2 w3 = f * f * f;
+
+        float2 w12 = w1 + w2;
+
+        // Compute uv coordinates for sampling the texture
+        float2 tc0 = (tc1 - 1.0f) * rcpTextureSize;
+        float2 tc3 = (tc1 + 2.0f) * rcpTextureSize;
+        float2 tc12 = (tc1 + w2 / w12) * rcpTextureSize;
+
+        // Compute sample weights
+        float sw0 = w12.x * w12.y; // middle
+        float sw1 = w12.x * w0.y;  // top
+        float sw2 = w0.x  * w12.y; // left
+        float sw3 = w12.x * w3.y;  // bottom
+        float sw4 = w3.x  * w12.y; // right
+
+        // total weight of samples to normalize result.
+        float totalWeight = sw0 + sw1 + sw2 + sw3 + sw4;
+
+        float result = 0.0f;
+        result += texture.SampleLevel(textureSampler, float2(tc12.x, tc12.y), 0.0).r * sw0;
+        result += texture.SampleLevel(textureSampler, float2(tc12.x,  tc0.y), 0.0).r * sw1;
+        result += texture.SampleLevel(textureSampler, float2( tc0.x, tc12.y), 0.0).r * sw2;
+        result += texture.SampleLevel(textureSampler, float2(tc12.x,  tc3.y), 0.0).r * sw3;
+        result += texture.SampleLevel(textureSampler, float2( tc3.x, tc12.y), 0.0).r * sw4;
+
+        return result / totalWeight;
     }
-    else
+
+    float2 GetWorldXYPosition(in ObjectSrg::PatchData patchData, in float2 vertexPosition)
     {
-        height = ViewSrg::m_heightmapImage.SampleLevel(ViewSrg::HeightmapSampler, uv, 0).r;
+        return float2(patchData.m_xyTranslation + vertexPosition * patchData.m_xyScale);
     }
 
-    return ObjectSrg::m_terrainData.m_heightScale * (height - 0.5f);
-}
+    float2 GetHeightmapUv(in float2 position, in float2 worldMin, in float2 worldMax)
+    {
+        return (position - worldMin) / (worldMax - worldMin);
+    }
 
-float3 GetTerrainWorldPosition(ObjectSrg::TerrainData terrainData, float2 vertexPosition, float2 uv)
-{
-    // Remove all vertices outside our bounds by turning them into NaN positions.
-    if (any(uv > 1.0) || any (uv < 0.0))
+    int2 GetHeightmapCoord(in float2 position, in float2 rcpSampleSpacing, in float2 worldMin)
     {
-        return asfloat(0x7fc00000); // NaN
+        return int2((position - worldMin) * rcpSampleSpacing);
     }
 
-    // Loop up the height and calculate our final position.
-    float height = GetHeight(uv);
-    return mul(GetObject_WorldMatrix(), float4(vertexPosition, height, 1.0f)).xyz;
-}
+    float GetHeight(Texture2D heightmapImage, int2 offset = int2(0, 0))
+    {
+        float height = heightmapImage.Load(int3(m_heightmapCoord + offset, 0)).r;
+        return m_worldMin.z + height * m_heightScale;
+    }
 
-float4 GetTerrainProjectedPosition(ObjectSrg::TerrainData terrainData, float2 vertexPosition, float2 uv)
-{
-    return mul(ViewSrg::m_viewProjectionMatrix, float4(GetTerrainWorldPosition(terrainData, vertexPosition, uv), 1.0));
-}
+    float GetSmoothedHeight(Texture2D heightmapImage, SamplerState heightmapSampler)
+    {
+        float2 uv = GetHeightmapUv(m_xyPosition, m_worldMin.xy, m_worldMax.xy);
+        float2 halfStep = m_rcpTextureSize * 0.5;
+        uv = uv * (1.0 - m_rcpTextureSize) + halfStep;
+        float height = SampleBSpline5Tap(heightmapImage, heightmapSampler, uv, m_textureSize, m_rcpTextureSize);
+        return m_worldMin.z + height * (m_worldMax.z - m_worldMin.z);
+    }
+
+    float3 CalculateNormal(Texture2D heightmapImage)
+    {
+        float up    = GetHeight(heightmapImage, int2( 0, -1));
+        float right = GetHeight(heightmapImage, int2( 1,  0));
+        float down  = GetHeight(heightmapImage, int2( 0,  1));
+        float left  = GetHeight(heightmapImage, int2(-1,  0));
+
+        float3 bitangent = normalize(float3(0.0, m_sampleSpacing.y * 2.0f, down - up));
+        float3 tangent = normalize(float3(m_sampleSpacing.x * 2.0f, 0.0, right - left));
+        return normalize(cross(tangent, bitangent));
+    }
+
+    bool IsVertexOutsideOfTerrainBounds()
+    {
+        return (any(m_xyPosition < m_worldMin.xy) ||
+                any(m_xyPosition > m_worldMax.xy));
+    }
+
+    void Initialize(Texture2D heightmapImage, float2 vertexPosition, ObjectSrg::PatchData patchData, float3 worldMin, float3 worldMax)
+    {
+        m_worldMin = worldMin;
+        m_worldMax = worldMax;
+        m_xyPosition = GetWorldXYPosition(patchData, vertexPosition);
+
+        heightmapImage.GetDimensions(m_textureSize.x, m_textureSize.y);
+        m_rcpTextureSize = rcp(m_textureSize);
+        m_sampleSpacing = (worldMax.xy - worldMin.xy) * m_rcpTextureSize;
+        m_rcpSampleSpacing = rcp(m_sampleSpacing);
+
+        m_heightScale = worldMax.z - worldMin.z;
+        m_heightmapCoord = GetHeightmapCoord(m_xyPosition, m_rcpSampleSpacing, worldMin.xy);
+    }
+};

+ 7 - 7
Gems/Terrain/Assets/Shaders/Terrain/TerrainDetailHelpers.azsli

@@ -123,7 +123,7 @@ float3 GetDetailColor(TerrainSrg::DetailMaterialData materialData, float2 uv, fl
     float3 color = materialData.m_baseColor;
     if ((materialData.m_flags & DetailTextureFlags::UseTextureBaseColor) > 0)
     {
-        color = TerrainSrg::m_detailTextures[GetDetailColorIndex(materialData)].SampleGrad(TerrainMaterialSrg::m_sampler, uv, ddx, ddy).rgb;
+        color = TerrainSrg::m_textures[GetDetailColorIndex(materialData)].SampleGrad(TerrainMaterialSrg::m_sampler, uv, ddx, ddy).rgb;
     }
     return color * materialData.m_baseColorFactor;
 }
@@ -133,7 +133,7 @@ float3 GetDetailNormal(TerrainSrg::DetailMaterialData materialData, float2 uv, f
     float2 normal = float2(0.0, 0.0);
     if ((materialData.m_flags & DetailTextureFlags::UseTextureNormal) > 0)
     {
-        normal = TerrainSrg::m_detailTextures[GetDetailNormalIndex(materialData)].SampleGrad(TerrainMaterialSrg::m_sampler, uv, ddx, ddy).rg;
+        normal = TerrainSrg::m_textures[GetDetailNormalIndex(materialData)].SampleGrad(TerrainMaterialSrg::m_sampler, uv, ddx, ddy).rg;
     }
     
     // X and Y are inverted here to be consistent with SampleNormalXY in NormalInput.azsli.
@@ -153,7 +153,7 @@ float GetDetailRoughness(TerrainSrg::DetailMaterialData materialData, float2 uv,
     float roughness = materialData.m_roughnessScale;
     if ((materialData.m_flags & DetailTextureFlags::UseTextureRoughness) > 0)
     {
-        roughness = TerrainSrg::m_detailTextures[GetDetailRoughnessIndex(materialData)].SampleGrad(TerrainMaterialSrg::m_sampler, uv, ddx, ddy).r;
+        roughness = TerrainSrg::m_textures[GetDetailRoughnessIndex(materialData)].SampleGrad(TerrainMaterialSrg::m_sampler, uv, ddx, ddy).r;
         roughness = materialData.m_roughnessBias + roughness * materialData.m_roughnessScale;
     }
     return roughness;
@@ -164,7 +164,7 @@ float GetDetailMetalness(TerrainSrg::DetailMaterialData materialData, float2 uv,
     float metalness = 1.0;
     if ((materialData.m_flags & DetailTextureFlags::UseTextureMetallic) > 0)
     {
-        metalness = TerrainSrg::m_detailTextures[GetDetailMetalnessIndex(materialData)].SampleGrad(TerrainMaterialSrg::m_sampler, uv, ddx, ddy).r;
+        metalness = TerrainSrg::m_textures[GetDetailMetalnessIndex(materialData)].SampleGrad(TerrainMaterialSrg::m_sampler, uv, ddx, ddy).r;
     }
     return metalness * materialData.m_metalFactor;
 }
@@ -174,7 +174,7 @@ float GetDetailSpecularF0(TerrainSrg::DetailMaterialData materialData, float2 uv
     float specularF0 = 1.0;
     if ((materialData.m_flags & DetailTextureFlags::UseTextureSpecularF0) > 0)
     {
-        specularF0 = TerrainSrg::m_detailTextures[GetDetailSpecularF0Index(materialData)].SampleGrad(TerrainMaterialSrg::m_sampler, uv, ddx, ddy).r;
+        specularF0 = TerrainSrg::m_textures[GetDetailSpecularF0Index(materialData)].SampleGrad(TerrainMaterialSrg::m_sampler, uv, ddx, ddy).r;
     }
     return specularF0 * materialData.m_specularF0Factor;
 }
@@ -184,7 +184,7 @@ float GetDetailOcclusion(TerrainSrg::DetailMaterialData materialData, float2 uv,
     float occlusion = 1.0;
     if ((materialData.m_flags & DetailTextureFlags::UseTextureOcclusion) > 0)
     {
-        occlusion = TerrainSrg::m_detailTextures[GetDetailOcclusionIndex(materialData)].SampleGrad(TerrainMaterialSrg::m_sampler, uv, ddx, ddy).r;
+        occlusion = TerrainSrg::m_textures[GetDetailOcclusionIndex(materialData)].SampleGrad(TerrainMaterialSrg::m_sampler, uv, ddx, ddy).r;
     }
     return occlusion * materialData.m_occlusionFactor;
 }
@@ -194,7 +194,7 @@ float GetDetailHeight(TerrainSrg::DetailMaterialData materialData, float2 uv, fl
     float height = materialData.m_heightFactor;
     if ((materialData.m_flags & DetailTextureFlags::UseTextureHeight) > 0)
     {
-        height = TerrainSrg::m_detailTextures[GetDetailHeightIndex(materialData)].SampleGrad(TerrainMaterialSrg::m_sampler, uv, ddx, ddy).r;
+        height = TerrainSrg::m_textures[GetDetailHeightIndex(materialData)].SampleGrad(TerrainMaterialSrg::m_sampler, uv, ddx, ddy).r;
         height = materialData.m_heightOffset + height * materialData.m_heightFactor;
     }
     return height;

+ 69 - 48
Gems/Terrain/Assets/Shaders/Terrain/TerrainPBR_ForwardPass.azsl

@@ -8,6 +8,7 @@
 
 #include <Atom/Features/SrgSemantics.azsli>
 
+#include <scenesrg.srgi>
 #include <viewsrg.srgi>
 #include <TerrainSrg.azsli>
 #include <TerrainCommon.azsli>
@@ -26,7 +27,6 @@ struct VSOutput
     float4 m_position : SV_Position;
     float3 m_normal: NORMAL;
     float3 m_worldPosition : UV0;
-    float2 m_uv : UV1;
     float3 m_shadowCoords[ViewSrg::MaxCascadeCount] : UV2;
 };
 
@@ -34,24 +34,30 @@ VSOutput TerrainPBR_MainPassVS(VertexInput IN)
 {
     VSOutput OUT;
  
-    ObjectSrg::TerrainData terrainData = ObjectSrg::m_terrainData;
+    HeightContext heightContext;
+    heightContext.Initialize(SceneSrg::m_heightmapImage, IN.m_position, ObjectSrg::m_patchData, SceneSrg::m_terrainWorldData.m_min, SceneSrg::m_terrainWorldData.m_max);
 
-    float2 uv = IN.m_uv;
-    float2 origUv = lerp(terrainData.m_uvMin, terrainData.m_uvMax, uv);
-    float3 worldPosition = GetTerrainWorldPosition(terrainData, IN.m_position, origUv);
-    OUT.m_position = mul(ViewSrg::m_viewProjectionMatrix, float4(worldPosition, 1.0));
-    OUT.m_worldPosition = worldPosition;
+    if (heightContext.IsVertexOutsideOfTerrainBounds())
+    {
+        // Output a NaN to remove this vertex.
+        OUT.m_position = 1.0 / 0.0;
+        return OUT;
+    }
 
-    // Calculate normal
-    float up    = GetHeight(origUv + terrainData.m_uvStep * float2( 0.0f, -1.0f));
-    float right = GetHeight(origUv + terrainData.m_uvStep * float2( 1.0f,  0.0f));
-    float down  = GetHeight(origUv + terrainData.m_uvStep * float2( 0.0f,  1.0f));
-    float left  = GetHeight(origUv + terrainData.m_uvStep * float2(-1.0f,  0.0f));
+    float height = 0.0;
 
-    float3 bitangent = normalize(float3(0.0, terrainData.m_sampleSpacing * 2.0f, down - up));
-    float3 tangent = normalize(float3(terrainData.m_sampleSpacing * 2.0f, 0.0, right - left));
-    OUT.m_normal = normalize(cross(tangent, bitangent));
-    OUT.m_uv = uv;
+    if (o_useTerrainSmoothing)
+    {
+        height = heightContext.GetSmoothedHeight(SceneSrg::m_heightmapImage, SceneSrg::HeightmapSampler);
+    }
+    else
+    {
+        height = heightContext.GetHeight(SceneSrg::m_heightmapImage);
+    }
+
+    OUT.m_worldPosition = float3(heightContext.m_xyPosition, height);
+    OUT.m_position = mul(ViewSrg::m_viewProjectionMatrix, float4(OUT.m_worldPosition, 1.0));
+    OUT.m_normal = heightContext.CalculateNormal(SceneSrg::m_heightmapImage);
 
     // directional light shadow 
     const uint shadowIndex = ViewSrg::m_shadowIndexDirectionalLight;
@@ -59,7 +65,7 @@ VSOutput TerrainPBR_MainPassVS(VertexInput IN)
     {
         DirectionalLightShadow::GetShadowCoords(
             shadowIndex,
-            worldPosition,
+            OUT.m_worldPosition,
             OUT.m_normal,
             OUT.m_shadowCoords);
     }
@@ -76,7 +82,7 @@ ForwardPassOutput TerrainPBR_MainPassPS(VSOutput IN)
 
     float viewDistance = length(ViewSrg::m_worldPosition - surface.position);
     float detailFactor = saturate((viewDistance - TerrainMaterialSrg::m_detailFadeDistance) / max(TerrainMaterialSrg::m_detailFadeLength, EPSILON));
-    float2 detailUv = IN.m_uv * TerrainMaterialSrg::m_detailTextureMultiplier;
+    float2 detailUv = IN.m_worldPosition.xy * TerrainMaterialSrg::m_detailTextureMultiplier;
     
     // ------- Normal -------
     float3 macroNormal = normalize(IN.m_normal);
@@ -84,39 +90,54 @@ ForwardPassOutput TerrainPBR_MainPassPS(VSOutput IN)
     // ------- Macro Color / Normal -------
     float3 macroColor = TerrainMaterialSrg::m_baseColor.rgb;
 
-    // There's a bug that shows up with an NVidia GTX 1660 Super card happening on driver versions as recent as 496.49 (10/26/21) in which
-    // the IN.m_uv values will intermittently "flicker" to 0.0 after entering and exiting game mode.  
-    // (See https://github.com/o3de/o3de/issues/5014)
-    // This bug has only shown up on PCs when using the DX12 RHI.  It doesn't show up with Vulkan or when capturing frames with PIX or
-    // RenderDoc.  Our best guess is that it is a driver bug.  The workaround is to use the IN.m_uv values in a calculation prior to the 
-    // point that we actually use them for macroUv below.  The "if(any(!isnan(IN.m_uv)))" seems to be sufficient for the workaround.  The
-    // if statement will always be true, but just the act of reading these values in the if statement makes the values stable.  Removing
-    // the if statement causes the flickering to occur using the steps documented in the bug.
-    if (any(!isnan(IN.m_uv)))
+    uint2 macroGridResolution = uint2(TerrainSrg::m_macroMaterialGrid.m_resolution >> 16, TerrainSrg::m_macroMaterialGrid.m_resolution & 0xFFFF);
+    float macroTileSize = TerrainSrg::m_macroMaterialGrid.m_tileSize;
+    float2 macroGridOffset = TerrainSrg::m_macroMaterialGrid.m_offset;
+    uint2 macroGridPosition = (surface.position.xy - macroGridOffset) / macroTileSize;
+
+    uint macroTileIndex = macroGridResolution.x * macroGridPosition.y + macroGridPosition.x;
+    static const uint NumMacroMaterialsPerTile = 4;
+    macroTileIndex *= NumMacroMaterialsPerTile;
+
+    [unroll] for (uint i = 0; i < NumMacroMaterialsPerTile; ++i)
     {
-        [unroll] for (uint i = 0; i < 4 && (i < ObjectSrg::m_macroMaterialCount); ++i)
+        TerrainSrg::MacroMaterialData macroMaterialData = TerrainSrg::m_macroMaterialData[macroTileIndex + i];
+        if ((macroMaterialData.m_flags & 1) == 0)
+        {
+            break; // No more macro materials for this tile
+        }
+
+        if (any(surface.position.xy < macroMaterialData.m_boundsMin) || any (surface.position.xy > macroMaterialData.m_boundsMax))
+        {
+            continue; // Macro material exists for this tile but is out of the bounds of this particular position
+        }
+
+        float2 macroUvSize = macroMaterialData.m_boundsMax - macroMaterialData.m_boundsMin;
+        macroUvSize.x = -macroUvSize.x;
+        float2 macroUv = (macroMaterialData.m_boundsMin - surface.position.xy) / macroUvSize;
+
+        // The macro uv gradient can vary massively over the quad because different pixels may choose different macro materials with different UVs.
+        // To fix, we use the world position scaled by the macro uv scale which should be fairly uniform across macro materials.
+        float2 macroUvScale = IN.m_worldPosition.xy / macroUvSize;
+        float2 ddx_macroUv = ddx(macroUvScale);
+        float2 ddy_macroUv = ddy(macroUvScale);
+
+        if (macroMaterialData.m_colorMapId != 0xFFFF)
+        {
+            macroColor = TerrainSrg::m_textures[macroMaterialData.m_colorMapId].SampleGrad(TerrainMaterialSrg::m_sampler, macroUv, ddx_macroUv, ddy_macroUv).rgb;
+            macroColor = TransformColor(macroColor, ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg);
+        }
+
+        if (macroMaterialData.m_normalMapId != 0xFFFF)
         {
-            float2 macroUvMin = ObjectSrg::m_macroMaterialData[i].m_uvMin;
-            float2 macroUvMax = ObjectSrg::m_macroMaterialData[i].m_uvMax;
-            float2 macroUv = lerp(macroUvMin, macroUvMax, IN.m_uv);
-            if (macroUv.x >= 0.0 && macroUv.x <= 1.0 && macroUv.y >= 0.0 && macroUv.y <= 1.0)
-            {
-                if ((ObjectSrg::m_macroMaterialData[i].m_mapsInUse & 1) > 0)
-                {
-                    macroColor = GetBaseColorInput(ObjectSrg::m_macroColorMap[i], TerrainMaterialSrg::m_sampler, macroUv, macroColor, true);
-                }
-                if ((ObjectSrg::m_macroMaterialData[i].m_mapsInUse & 2) > 0)
-                {
-                    bool flipX = ObjectSrg::m_macroMaterialData[i].m_flipNormalX;
-                    bool flipY = ObjectSrg::m_macroMaterialData[i].m_flipNormalY;
-                    float factor = ObjectSrg::m_macroMaterialData[i].m_normalFactor;
-                    
-                    float2 sampledValue = SampleNormalXY(ObjectSrg::m_macroNormalMap[i], TerrainMaterialSrg::m_sampler, macroUv, flipX, flipY);
-                    macroNormal = normalize(GetTangentSpaceNormal_Unnormalized(sampledValue.xy, factor));
-                }
-                break;
-            }
+            bool flipX = macroMaterialData.m_flags & 2;
+            bool flipY = macroMaterialData.m_flags & 4;
+            float factor = macroMaterialData.m_normalFactor;
+        
+            float2 sampledValue = SampleNormalXY(TerrainSrg::m_textures[macroMaterialData.m_normalMapId], TerrainMaterialSrg::m_sampler, macroUv, flipX, flipY);
+            macroNormal = normalize(GetTangentSpaceNormal_Unnormalized(sampledValue, factor));
         }
+        break;
     }
     
     // ------- Base Color -------

+ 27 - 1
Gems/Terrain/Assets/Shaders/Terrain/TerrainSrg.azsli

@@ -53,12 +53,38 @@ ShaderResourceGroup TerrainSrg : SRG_Terrain
         uint2 m_padding;
     };
 
+    struct MacroMaterialData
+    {
+        // bit 1 : Is this macro material used.
+        // bit 2 : flip normal x
+        // bit 3 : flip normal y
+        uint m_flags;
+
+        uint m_colorMapId;
+        uint m_normalMapId;
+        float m_normalFactor;
+        float2 m_boundsMin;
+        float2 m_boundsMax;
+    };
+
+    struct MacroMaterialGrid
+    {
+        uint m_resolution; // How many x/y tiles in grid. x & y stored in 16 bits each. Total number of entries in m_macroMaterialData will be x * y
+        float m_tileSize; // Size of a tile in meters.
+        float2 m_offset; // x/y offset of min x/y corner of grid.
+    };
+
     Texture2D<uint4> m_detailMaterialIdImage;
     StructuredBuffer<DetailMaterialData> m_detailMaterialData;
-    Texture2D m_detailTextures[]; // bindless array of all textures for detail materials
 
+    StructuredBuffer<MacroMaterialData> m_macroMaterialData;
+    MacroMaterialGrid m_macroMaterialGrid;
+    
+    Texture2D m_textures[]; // bindless array of all textures for detail and macro materials
     float2 m_detailMaterialIdImageCenter;
     float m_detailHalfPixelUv;
     float4 m_detailAabb;
 
 }
+
+static const float MacroMaterialsPerTile = 4;

+ 25 - 3
Gems/Terrain/Assets/Shaders/Terrain/Terrain_DepthPass.azsl

@@ -6,6 +6,7 @@
  */
 
 #include <Atom/Features/SrgSemantics.azsli>
+#include <scenesrg.srgi>
 #include <viewsrg.srgi>
 #include "TerrainCommon.azsli"
 #include <Atom/RPI/ShaderResourceGroups/DefaultDrawSrg.azsli>
@@ -18,9 +19,30 @@ struct VSDepthOutput
 VSDepthOutput MainVS(in VertexInput input)
 {
     VSDepthOutput output;
-    ObjectSrg::TerrainData terrainData = ObjectSrg::m_terrainData;
 
-    float2 origUv = lerp(terrainData.m_uvMin, terrainData.m_uvMax, input.m_uv);
-    output.m_position = GetTerrainProjectedPosition(terrainData, input.m_position, origUv);
+    HeightContext heightContext;
+    heightContext.Initialize(SceneSrg::m_heightmapImage, input.m_position, ObjectSrg::m_patchData, SceneSrg::m_terrainWorldData.m_min, SceneSrg::m_terrainWorldData.m_max);
+
+    if (heightContext.IsVertexOutsideOfTerrainBounds())
+    {
+        // Output a NaN to remove this vertex.
+        output.m_position = 1.0 / 0.0;
+        return output;
+    }
+
+    float height = 0.0;
+
+    if (o_useTerrainSmoothing)
+    {
+        height = heightContext.GetSmoothedHeight(SceneSrg::m_heightmapImage, SceneSrg::HeightmapSampler);
+    }
+    else
+    {
+        height = heightContext.GetHeight(SceneSrg::m_heightmapImage);
+    }
+
+    float3 worldPosition = float3(heightContext.m_xyPosition, height);
+    output.m_position = mul(ViewSrg::m_viewProjectionMatrix, float4(worldPosition, 1.0));
+    
     return output;
 }

+ 0 - 80
Gems/Terrain/Assets/Shaders/Terrain/ViewSrg.azsli

@@ -1,80 +0,0 @@
-/*
- * Copyright (c) Contributors to the Open 3D Engine Project.
- * For complete copyright and license terms please see the LICENSE at the root of this distribution.
- *
- * SPDX-License-Identifier: Apache-2.0 OR MIT
- *
- */
-
-#ifndef AZ_COLLECTING_PARTIAL_SRGS
-#error Do not include this file directly. Include the main .srgi file instead.
-#endif
-
-partial ShaderResourceGroup ViewSrg
-{
-    Sampler HeightmapSampler
-    {
-        MinFilter = Linear;
-        MagFilter = Linear;
-        MipFilter = Point;
-        AddressU = Clamp;
-        AddressV = Clamp;
-        AddressW = Clamp;
-    };
-
-    Sampler DetailSampler
-    {
-        AddressU = Wrap;
-        AddressV = Wrap;
-        MinFilter = Point;
-        MagFilter = Point;
-        MipFilter = Point;
-    };
-
-    struct DetailMaterialData
-    {
-        // Uv
-        row_major float3x4 m_uvTransform;
-
-        float3 m_baseColor;
-
-        // Factor / Scale / Bias for input textures
-        float m_baseColorFactor;
-
-        float m_normalFactor;
-        float m_metalFactor;
-        float m_roughnessScale;
-        float m_roughnessBias;
-
-        float m_specularF0Factor;
-        float m_occlusionFactor;
-        float m_heightFactor;
-        float m_heightOffset;
-
-        float m_heightBlendFactor;
-
-        // Flags
-        uint m_flags; // see DetailTextureFlags
-
-        // Image indices
-        uint m_colorNormalImageIndices;
-        uint m_roughnessMetalnessImageIndices;
-
-        uint m_specularF0OcclusionImageIndices;
-        uint m_heightImageIndex; // only first 16 bits used
-
-        // 16 byte aligned
-        uint2 m_padding;
-    };
-
-    Texture2D m_heightmapImage;
-    Texture2D<uint4> m_detailMaterialIdImage;
-    StructuredBuffer<DetailMaterialData> m_detailMaterialData;
-    
-    Texture2D m_detailTextures[]; // bindless array of all textures for detail materials
-
-    float2 m_detailMaterialIdImageCenter;
-    float m_detailHalfPixelUv;
-    
-    float4 m_detailAabb;
-}

+ 44 - 0
Gems/Terrain/Code/Source/TerrainRenderer/Aabb2i.cpp

@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <TerrainRenderer/Aabb2i.h>
+#include <AzCore/Math/MathUtils.h>
+
+namespace Terrain
+{
+    Aabb2i::Aabb2i(const Vector2i& min, const Vector2i& max)
+        : m_min(min)
+        , m_max(max)
+    {}
+
+    Aabb2i Aabb2i::operator+(const Vector2i& rhs) const
+    {
+        return { m_min + rhs, m_max + rhs };
+    }
+
+    Aabb2i Aabb2i::operator-(const Vector2i& rhs) const
+    {
+        return *this + -rhs;
+    }
+
+    Aabb2i Aabb2i::GetClamped(Aabb2i rhs) const
+    {
+        Aabb2i ret;
+        ret.m_min.m_x = AZ::GetMax(m_min.m_x, rhs.m_min.m_x);
+        ret.m_min.m_y = AZ::GetMax(m_min.m_y, rhs.m_min.m_y);
+        ret.m_max.m_x = AZ::GetMin(m_max.m_x, rhs.m_max.m_x);
+        ret.m_max.m_y = AZ::GetMin(m_max.m_y, rhs.m_max.m_y);
+        return ret;
+    }
+
+    bool Aabb2i::IsValid() const
+    {
+        // Intentionally strict, equal min/max not valid.
+        return m_min.m_x < m_max.m_x && m_min.m_y < m_max.m_y;
+    }
+}

+ 34 - 0
Gems/Terrain/Code/Source/TerrainRenderer/Aabb2i.h

@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <TerrainRenderer/Vector2i.h>
+#include <AzCore/std/limits.h>
+
+namespace Terrain
+{
+    class Aabb2i
+    {
+    public:
+
+        Aabb2i() = default;
+        Aabb2i(const Vector2i& min, const Vector2i& max);
+
+        Aabb2i operator+(const Vector2i& offset) const;
+        Aabb2i operator-(const Vector2i& offset) const;
+
+        Aabb2i GetClamped(Aabb2i rhs) const;
+        bool IsValid() const;
+
+        
+        Vector2i m_min{AZStd::numeric_limits<int32_t>::min(), AZStd::numeric_limits<int32_t>::min()};
+        Vector2i m_max{AZStd::numeric_limits<int32_t>::max(), AZStd::numeric_limits<int32_t>::max()};
+
+    };
+}

+ 101 - 0
Gems/Terrain/Code/Source/TerrainRenderer/BindlessImageArrayHandler.cpp

@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <TerrainRenderer/BindlessImageArrayHandler.h>
+#include <Atom/RPI.Reflect/Image/Image.h>
+#include <Atom/RPI.Public/Image/ImageSystemInterface.h>
+
+namespace AZ::Render
+{
+    namespace
+    {
+        [[maybe_unused]] const char* BindlessImageArrayHandlerName = "TerrainFeatureProcessor";
+    }
+
+    void BindlessImageArrayHandler::Initialize(AZ::Data::Instance<AZ::RPI::ShaderResourceGroup>& srg, const AZ::Name& propertyName)
+    {
+        if (!m_isInitialized)
+        {
+            m_isInitialized = UpdateSrgIndices(srg, propertyName);
+        }
+        else
+        {
+            AZ_Error(BindlessImageArrayHandlerName, false, "Already initialized.");
+        }
+    }
+
+    void BindlessImageArrayHandler::Reset()
+    {
+        m_texturesIndex = {};
+        m_isInitialized = false;
+    }
+
+    bool BindlessImageArrayHandler::IsInitialized() const
+    {
+        return m_isInitialized;
+    }
+
+    bool BindlessImageArrayHandler::UpdateSrgIndices(AZ::Data::Instance<AZ::RPI::ShaderResourceGroup>& srg, const AZ::Name& propertyName)
+    {
+        if (srg)
+        {
+            m_texturesIndex = srg->GetLayout()->FindShaderInputImageUnboundedArrayIndex(propertyName);
+            AZ_Error(BindlessImageArrayHandlerName, m_texturesIndex.IsValid(), "Failed to find srg input constant %s.", propertyName.GetCStr());
+        }
+        else
+        {
+            AZ_Error(BindlessImageArrayHandlerName, false, "Cannot initialize using a null shader resource group.");
+        }
+        return m_texturesIndex.IsValid();
+    }
+
+    uint16_t BindlessImageArrayHandler::AppendBindlessImage(const AZ::RHI::ImageView* imageView)
+    {
+        uint16_t imageIndex = 0xFFFF;
+
+        AZStd::unique_lock<AZStd::shared_mutex> lock(m_updateMutex);
+        if (m_bindlessImageViewFreeList.size() > 0)
+        {
+            imageIndex = m_bindlessImageViewFreeList.back();
+            m_bindlessImageViewFreeList.pop_back();
+            m_bindlessImageViews.at(imageIndex) = imageView;
+        }
+        else
+        {
+            imageIndex = aznumeric_cast<uint16_t>(m_bindlessImageViews.size());
+            m_bindlessImageViews.push_back(imageView);
+        }
+        return imageIndex;
+    }
+
+    void BindlessImageArrayHandler::UpdateBindlessImage(uint16_t index, const AZ::RHI::ImageView* imageView)
+    {
+        AZStd::shared_lock<AZStd::shared_mutex> lock(m_updateMutex);
+        m_bindlessImageViews.at(index) = imageView;
+    }
+
+    void BindlessImageArrayHandler::RemoveBindlessImage(uint16_t index)
+    {
+        AZStd::unique_lock<AZStd::shared_mutex> lock(m_updateMutex);
+        m_bindlessImageViews.at(index) = AZ::RPI::ImageSystemInterface::Get()->GetSystemImage(AZ::RPI::SystemImage::Magenta)->GetImageView();
+        m_bindlessImageViewFreeList.push_back(index);
+    }
+
+    bool BindlessImageArrayHandler::UpdateSrg(AZ::Data::Instance<AZ::RPI::ShaderResourceGroup>& srg) const
+    {
+        if (!m_isInitialized)
+        {
+            AZ_Error("BindlessImageArrayHandler", false, "BindlessImageArrayHandler not initialized")
+            return false;
+        }
+
+        AZStd::array_view<const AZ::RHI::ImageView*> imageViews(m_bindlessImageViews.data(), m_bindlessImageViews.size());
+        return srg->SetImageViewUnboundedArray(m_texturesIndex, imageViews);
+    }
+
+}

+ 48 - 0
Gems/Terrain/Code/Source/TerrainRenderer/BindlessImageArrayHandler.h

@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <AzCore/std/containers/vector.h>
+
+#include <Atom/RHI/ImageView.h>
+#include <Atom/RHI.Reflect/ShaderResourceGroupLayoutDescriptor.h>
+#include <Atom/RPI.Public/Shader/ShaderResourceGroup.h>
+
+namespace AZ::Render
+{
+    class BindlessImageArrayHandler
+    {
+        public:
+            
+            static constexpr uint16_t InvalidImageIndex = 0xFFFF;
+
+            BindlessImageArrayHandler() = default;
+            ~BindlessImageArrayHandler() = default;
+
+            void Initialize(AZ::Data::Instance<AZ::RPI::ShaderResourceGroup>& srg, const AZ::Name& propertyName);
+            void Reset();
+            bool IsInitialized() const;
+            bool UpdateSrgIndices(AZ::Data::Instance<AZ::RPI::ShaderResourceGroup>& srg, const AZ::Name& propertyName);
+
+            uint16_t AppendBindlessImage(const RHI::ImageView* imageView);
+            void UpdateBindlessImage(uint16_t index, const RHI::ImageView* imageView);
+            void RemoveBindlessImage(uint16_t index);
+
+            bool UpdateSrg(AZ::Data::Instance<AZ::RPI::ShaderResourceGroup>& srg) const;
+            
+        private:
+
+            AZStd::vector<const RHI::ImageView*> m_bindlessImageViews;
+            AZStd::vector<uint16_t> m_bindlessImageViewFreeList;
+            RHI::ShaderInputImageUnboundedArrayIndex m_texturesIndex;
+            AZStd::shared_mutex m_updateMutex;
+            bool m_isInitialized{ false };
+    };
+}
+

+ 912 - 0
Gems/Terrain/Code/Source/TerrainRenderer/TerrainDetailMaterialManager.cpp

@@ -0,0 +1,912 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <TerrainRenderer/TerrainDetailMaterialManager.h>
+#include <TerrainRenderer/Components/TerrainSurfaceMaterialsListComponent.h>
+
+#include <AzCore/Console/Console.h>
+
+#include <Atom/RPI.Public/Image/AttachmentImagePool.h>
+#include <Atom/RPI.Public/Image/ImageSystemInterface.h>
+#include <Atom/RPI.Public/Shader/ShaderSystemInterface.h>
+
+#include <SurfaceData/SurfaceDataSystemRequestBus.h>
+
+namespace Terrain
+{
+    namespace
+    {
+        [[maybe_unused]] static const char* TerrainDetailMaterialManagerName = "TerrainDetailMaterialManager";
+        static const char* TerrainDetailChars = "TerrainDetail";
+    }
+    
+    namespace DetailMaterialInputs
+    {
+        static const char* const BaseColorColor("baseColor.color");
+        static const char* const BaseColorMap("baseColor.textureMap");
+        static const char* const BaseColorUseTexture("baseColor.useTexture");
+        static const char* const BaseColorFactor("baseColor.factor");
+        static const char* const BaseColorBlendMode("baseColor.textureBlendMode");
+        static const char* const MetallicMap("metallic.textureMap");
+        static const char* const MetallicUseTexture("metallic.useTexture");
+        static const char* const MetallicFactor("metallic.factor");
+        static const char* const RoughnessMap("roughness.textureMap");
+        static const char* const RoughnessUseTexture("roughness.useTexture");
+        static const char* const RoughnessFactor("roughness.factor");
+        static const char* const RoughnessLowerBound("roughness.lowerBound");
+        static const char* const RoughnessUpperBound("roughness.upperBound");
+        static const char* const SpecularF0Map("specularF0.textureMap");
+        static const char* const SpecularF0UseTexture("specularF0.useTexture");
+        static const char* const SpecularF0Factor("specularF0.factor");
+        static const char* const NormalMap("normal.textureMap");
+        static const char* const NormalUseTexture("normal.useTexture");
+        static const char* const NormalFactor("normal.factor");
+        static const char* const NormalFlipX("normal.flipX");
+        static const char* const NormalFlipY("normal.flipY");
+        static const char* const DiffuseOcclusionMap("occlusion.diffuseTextureMap");
+        static const char* const DiffuseOcclusionUseTexture("occlusion.diffuseUseTexture");
+        static const char* const DiffuseOcclusionFactor("occlusion.diffuseFactor");
+        static const char* const HeightMap("parallax.textureMap");
+        static const char* const HeightUseTexture("parallax.useTexture");
+        static const char* const HeightFactor("parallax.factor");
+        static const char* const HeightOffset("parallax.offset");
+        static const char* const HeightBlendFactor("parallax.blendFactor");
+    }
+    
+    namespace TerrainSrgInputs
+    {
+        static const char* const DetailMaterialIdImage("m_detailMaterialIdImage");
+        static const char* const DetailMaterialData("m_detailMaterialData");
+        static const char* const DetailMaterialIdImageCenter("m_detailMaterialIdImageCenter");
+        static const char* const DetailHalfPixelUv("m_detailHalfPixelUv");
+        static const char* const DetailAabb("m_detailAabb");
+    }
+    
+    AZ_CVAR(bool,
+        r_terrainDebugDetailMaterials,
+        false,
+        [](const bool& value)
+        {
+            AZ::RPI::ShaderSystemInterface::Get()->SetGlobalShaderOption(AZ::Name{ "o_debugDetailMaterialIds" }, AZ::RPI::ShaderOptionValue{ value });
+        },
+        AZ::ConsoleFunctorFlags::Null,
+        "Turns on debugging for detail material ids for terrain."
+    );
+    
+    AZ_CVAR(bool,
+        r_terrainDebugDetailImageUpdates,
+        false,
+        nullptr,
+        AZ::ConsoleFunctorFlags::Null,
+        "Turns on debugging for detail material update regions for terrain."
+    );
+
+    void TerrainDetailMaterialManager::Initialize(
+        const AZStd::shared_ptr<AZ::Render::BindlessImageArrayHandler>& bindlessImageHandler,
+        AZ::Data::Instance<AZ::RPI::ShaderResourceGroup>& terrainSrg)
+    {
+        AZ_Error(TerrainDetailMaterialManagerName, bindlessImageHandler, "bindlessImageHandler must not be null.");
+        AZ_Error(TerrainDetailMaterialManagerName, terrainSrg, "terrainSrg must not be null.");
+        AZ_Error(TerrainDetailMaterialManagerName, !m_isInitialized, "Already initialized.");
+
+        if (!bindlessImageHandler || !terrainSrg || m_isInitialized)
+        {
+            return;
+        }
+        
+        if (UpdateSrgIndices(terrainSrg))
+        {
+            m_bindlessImageHandler = bindlessImageHandler;
+            
+            // Find any detail material areas that have already been created.
+            TerrainAreaMaterialRequestBus::EnumerateHandlers(
+                [&](TerrainAreaMaterialRequests* handler)
+                {
+                    const AZ::Aabb& bounds = handler->GetTerrainSurfaceMaterialRegion();
+                    const AZStd::vector<TerrainSurfaceMaterialMapping> materialMappings = handler->GetSurfaceMaterialMappings();
+                    AZ::EntityId entityId = *(Terrain::TerrainAreaMaterialRequestBus::GetCurrentBusId());
+                
+                    DetailMaterialListRegion& materialRegion = FindOrCreateByEntityId(entityId, m_detailMaterialRegions);
+                    materialRegion.m_region = bounds;
+
+                    for (const auto& materialMapping : materialMappings)
+                    {
+                        if (materialMapping.m_materialInstance)
+                        {
+                            OnTerrainSurfaceMaterialMappingCreated(entityId, materialMapping.m_surfaceTag, materialMapping.m_materialInstance);
+                        }
+                    }
+                    return true;
+                }
+            );
+            TerrainAreaMaterialNotificationBus::Handler::BusConnect();
+            
+            AZ::Aabb worldBounds = AZ::Aabb::CreateNull();
+            AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
+                worldBounds, &AzFramework::Terrain::TerrainDataRequests::GetTerrainAabb);
+
+            OnTerrainDataChanged(worldBounds, TerrainDataChangedMask::SurfaceData);
+            AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusConnect();
+
+            m_isInitialized = true;
+        }
+    }
+    
+    bool TerrainDetailMaterialManager::UpdateSrgIndices(AZ::Data::Instance<AZ::RPI::ShaderResourceGroup>& terrainSrg)
+    {
+        const AZ::RHI::ShaderResourceGroupLayout* terrainSrgLayout = terrainSrg->GetLayout();
+            
+        m_detailMaterialIdPropertyIndex = terrainSrgLayout->FindShaderInputImageIndex(AZ::Name(TerrainSrgInputs::DetailMaterialIdImage));
+        AZ_Error(TerrainDetailMaterialManagerName, m_detailMaterialIdPropertyIndex.IsValid(), "Failed to find terrain srg input constant %s.", TerrainSrgInputs::DetailMaterialIdImage);
+        
+        m_detailCenterPropertyIndex = terrainSrgLayout->FindShaderInputConstantIndex(AZ::Name(TerrainSrgInputs::DetailMaterialIdImageCenter));
+        AZ_Error(TerrainDetailMaterialManagerName, m_detailCenterPropertyIndex.IsValid(), "Failed to find terrain srg input constant %s.", TerrainSrgInputs::DetailMaterialIdImageCenter);
+
+        m_detailHalfPixelUvPropertyIndex = terrainSrgLayout->FindShaderInputConstantIndex(AZ::Name(TerrainSrgInputs::DetailHalfPixelUv));
+        AZ_Error(TerrainDetailMaterialManagerName, m_detailHalfPixelUvPropertyIndex.IsValid(), "Failed to find terrain srg input constant %s.", TerrainSrgInputs::DetailHalfPixelUv);
+        
+        m_detailAabbPropertyIndex = terrainSrgLayout->FindShaderInputConstantIndex(AZ::Name(TerrainSrgInputs::DetailAabb));
+        AZ_Error(TerrainDetailMaterialManagerName, m_detailAabbPropertyIndex.IsValid(), "Failed to find terrain srg input constant %s.", TerrainSrgInputs::DetailAabb);
+
+        // Set up the gpu buffer for detail material data
+        AZ::Render::GpuBufferHandler::Descriptor desc;
+        desc.m_bufferName = "Detail Material Data";
+        desc.m_bufferSrgName = TerrainSrgInputs::DetailMaterialData;
+        desc.m_elementSize = sizeof(DetailMaterialShaderData);
+        desc.m_srgLayout = terrainSrgLayout;
+        m_detailMaterialDataBuffer = AZ::Render::GpuBufferHandler(desc);
+
+        bool IndicesValid =
+            m_detailMaterialIdPropertyIndex.IsValid() &&
+            m_detailCenterPropertyIndex.IsValid() &&
+            m_detailHalfPixelUvPropertyIndex.IsValid() &&
+            m_detailAabbPropertyIndex.IsValid();
+
+        m_detailImageNeedsUpdate = true;
+        m_detailMaterialBufferNeedsUpdate = true;
+
+        return IndicesValid && m_detailMaterialDataBuffer.IsValid();
+    }
+    
+    void TerrainDetailMaterialManager::RemoveAllImages()
+    {   
+        for (const DetailMaterialData& materialData: m_detailMaterials.GetDataVector())
+        {
+            DetailMaterialShaderData& shaderData = m_detailMaterialShaderData.GetElement(materialData.m_detailMaterialBufferIndex);
+
+            auto checkRemoveImage = [&](uint16_t index)
+            {
+                if (index != 0xFFFF)
+                {
+                    m_bindlessImageHandler->RemoveBindlessImage(index);
+                }
+            };
+            
+            checkRemoveImage(shaderData.m_colorImageIndex);
+            checkRemoveImage(shaderData.m_normalImageIndex);
+            checkRemoveImage(shaderData.m_roughnessImageIndex);
+            checkRemoveImage(shaderData.m_metalnessImageIndex);
+            checkRemoveImage(shaderData.m_specularF0ImageIndex);
+            checkRemoveImage(shaderData.m_occlusionImageIndex);
+            checkRemoveImage(shaderData.m_heightImageIndex);
+        }
+    }
+
+    bool TerrainDetailMaterialManager::IsInitialized() const
+    {
+        return m_isInitialized;
+    }
+
+    void TerrainDetailMaterialManager::Reset()
+    {
+        RemoveAllImages();
+        m_bindlessImageHandler.reset();
+
+        m_detailTextureImage = {};
+        m_detailMaterials.Clear();
+        m_detailMaterialRegions.Clear();
+        m_detailMaterialShaderData.Clear();
+        m_detailMaterialDataBuffer.Release();
+
+        m_dirtyDetailRegion = AZ::Aabb::CreateNull();
+        m_previousCameraPosition = AZ::Vector3(AZStd::numeric_limits<float>::max(), 0.0, 0.0);
+        m_detailTextureBounds = {};
+        m_detailTextureCenter = {};
+
+        m_detailMaterialBufferNeedsUpdate = false;
+        m_detailImageNeedsUpdate = false;
+
+        TerrainAreaMaterialNotificationBus::Handler::BusDisconnect();
+        AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusDisconnect();
+
+        m_isInitialized = false;
+    }
+
+    void TerrainDetailMaterialManager::Update(const AZ::Vector3& cameraPosition, AZ::Data::Instance<AZ::RPI::ShaderResourceGroup>& terrainSrg)
+    {
+        if (m_detailMaterialBufferNeedsUpdate)
+        {
+            m_detailMaterialBufferNeedsUpdate = false;
+            m_detailMaterialDataBuffer.UpdateBuffer(m_detailMaterialShaderData.GetRawData(), aznumeric_cast<uint32_t>(m_detailMaterialShaderData.GetSize()));
+        }
+        
+        if (m_dirtyDetailRegion.IsValid() || !cameraPosition.IsClose(m_previousCameraPosition) || m_detailImageNeedsUpdate)
+        {
+            if (r_terrainDebugDetailImageUpdates)
+            {
+                AZ_Printf("TerrainDetailMaterialManager", "Previous Camera: (%f, %f, %f) New Cameara: (%f, %f, %f)",
+                    m_previousCameraPosition.GetX(), m_previousCameraPosition.GetY(), m_previousCameraPosition.GetZ(),
+                    cameraPosition.GetX(), cameraPosition.GetY(), cameraPosition.GetZ());
+            }
+            int32_t newDetailTexturePosX = aznumeric_cast<int32_t>(AZStd::roundf(cameraPosition.GetX() / DetailTextureScale));
+            int32_t newDetailTexturePosY = aznumeric_cast<int32_t>(AZStd::roundf(cameraPosition.GetY() / DetailTextureScale));
+            
+            Aabb2i newBounds;
+            newBounds.m_min.m_x = newDetailTexturePosX - DetailTextureSizeHalf;
+            newBounds.m_min.m_y = newDetailTexturePosY - DetailTextureSizeHalf;
+            newBounds.m_max.m_x = newDetailTexturePosX + DetailTextureSizeHalf;
+            newBounds.m_max.m_y = newDetailTexturePosY + DetailTextureSizeHalf;
+
+            // Use modulo to find the center point in texture space. Care must be taken so negative values are
+            // handled appropriately (ie, we want -1 % 1024 to equal 1023, not -1)
+            Vector2i newCenter;
+            newCenter.m_x = (DetailTextureSize + (newDetailTexturePosX % DetailTextureSize)) % DetailTextureSize;
+            newCenter.m_y = (DetailTextureSize + (newDetailTexturePosY % DetailTextureSize)) % DetailTextureSize;
+
+            CheckUpdateDetailTexture(newBounds, newCenter);
+                
+            m_detailTextureBounds = newBounds;
+            m_dirtyDetailRegion = AZ::Aabb::CreateNull();
+
+            m_previousCameraPosition = cameraPosition;
+
+            AZ::Vector4 detailAabb = AZ::Vector4(
+                m_detailTextureBounds.m_min.m_x * DetailTextureScale,
+                m_detailTextureBounds.m_min.m_y * DetailTextureScale,
+                m_detailTextureBounds.m_max.m_x * DetailTextureScale,
+                m_detailTextureBounds.m_max.m_y * DetailTextureScale
+            );
+            AZ::Vector2 detailUvOffset = AZ::Vector2(float(newCenter.m_x) / DetailTextureSize, float(newCenter.m_y) / DetailTextureSize);
+
+            terrainSrg->SetConstant(m_detailAabbPropertyIndex, detailAabb);
+            terrainSrg->SetConstant(m_detailHalfPixelUvPropertyIndex, 0.5f / DetailTextureSize);
+            terrainSrg->SetConstant(m_detailCenterPropertyIndex, detailUvOffset);
+            terrainSrg->SetImage(m_detailMaterialIdPropertyIndex, m_detailTextureImage);
+
+            m_detailMaterialDataBuffer.UpdateSrg(terrainSrg.get());
+        }
+        
+        m_detailImageNeedsUpdate = false;
+    }
+
+    void TerrainDetailMaterialManager::OnTerrainDataChanged(const AZ::Aabb& dirtyRegion, TerrainDataChangedMask dataChangedMask)
+    {
+        if ((dataChangedMask & TerrainDataChangedMask::SurfaceData) != 0)
+        {
+            m_dirtyDetailRegion.AddAabb(dirtyRegion);
+        }
+    }
+    
+    void TerrainDetailMaterialManager::OnTerrainSurfaceMaterialMappingCreated(AZ::EntityId entityId, SurfaceData::SurfaceTag surfaceTag, MaterialInstance material)
+    {
+        DetailMaterialListRegion& materialRegion = FindOrCreateByEntityId(entityId, m_detailMaterialRegions);
+
+        // Validate that the surface tag is new
+        for (DetailMaterialSurface& surface : materialRegion.m_materialsForSurfaces)
+        {
+            if (surface.m_surfaceTag == surfaceTag)
+            {
+                AZ_Error(TerrainDetailMaterialManagerName, false, "Already have a surface material mapping for this surface tag.");
+                return;
+            }
+        }
+
+        uint16_t detailMaterialId = CreateOrUpdateDetailMaterial(material);
+        materialRegion.m_materialsForSurfaces.push_back({ surfaceTag, detailMaterialId });
+        m_detailMaterials.GetData(detailMaterialId).refCount++;
+        m_dirtyDetailRegion.AddAabb(materialRegion.m_region);
+    }
+    
+    void TerrainDetailMaterialManager::OnTerrainSurfaceMaterialMappingDestroyed(AZ::EntityId entityId, SurfaceData::SurfaceTag surfaceTag)
+    {
+        DetailMaterialListRegion& materialRegion = FindOrCreateByEntityId(entityId, m_detailMaterialRegions);
+
+        for (DetailMaterialSurface& surface : materialRegion.m_materialsForSurfaces)
+        {
+            if (surface.m_surfaceTag == surfaceTag)
+            {
+                CheckDetailMaterialForDeletion(surface.m_detailMaterialId);
+
+                if (surface.m_surfaceTag != materialRegion.m_materialsForSurfaces.back().m_surfaceTag)
+                {
+                    AZStd::swap(surface, materialRegion.m_materialsForSurfaces.back());
+                }
+                materialRegion.m_materialsForSurfaces.pop_back();
+                m_dirtyDetailRegion.AddAabb(materialRegion.m_region);
+                return;
+            }
+        }
+        AZ_Error(TerrainDetailMaterialManagerName, false, "Could not find surface tag to destroy for OnTerrainSurfaceMaterialMappingDestroyed().");
+    }
+
+    void TerrainDetailMaterialManager::OnTerrainSurfaceMaterialMappingChanged(AZ::EntityId entityId, SurfaceData::SurfaceTag surfaceTag, MaterialInstance material)
+    {
+        DetailMaterialListRegion& materialRegion = FindOrCreateByEntityId(entityId, m_detailMaterialRegions);
+
+        bool found = false;
+        uint16_t materialId = CreateOrUpdateDetailMaterial(material);
+        for (DetailMaterialSurface& surface : materialRegion.m_materialsForSurfaces)
+        {
+            if (surface.m_surfaceTag == surfaceTag)
+            {
+                found = true;
+                if (surface.m_detailMaterialId != materialId)
+                {
+                    ++m_detailMaterials.GetData(materialId).refCount;
+                    CheckDetailMaterialForDeletion(surface.m_detailMaterialId);
+                    surface.m_detailMaterialId = materialId;
+                }
+                break;
+            }
+        }
+
+        if (!found)
+        {
+            ++m_detailMaterials.GetData(materialId).refCount;
+            materialRegion.m_materialsForSurfaces.push_back({ surfaceTag, materialId });
+        }
+        m_dirtyDetailRegion.AddAabb(materialRegion.m_region);
+    }
+
+    void TerrainDetailMaterialManager::OnTerrainSurfaceMaterialMappingRegionChanged(AZ::EntityId entityId, const AZ::Aabb& oldRegion, const AZ::Aabb& newRegion)
+    {
+        DetailMaterialListRegion& materialRegion = FindOrCreateByEntityId(entityId, m_detailMaterialRegions);
+        materialRegion.m_region = newRegion;
+        m_dirtyDetailRegion.AddAabb(oldRegion);
+        m_dirtyDetailRegion.AddAabb(newRegion);
+    }
+
+    void TerrainDetailMaterialManager::CheckDetailMaterialForDeletion(uint16_t detailMaterialId)
+    {
+        auto& detailMaterialData = m_detailMaterials.GetData(detailMaterialId);
+        if (--detailMaterialData.refCount == 0)
+        {
+            uint16_t bufferIndex = detailMaterialData.m_detailMaterialBufferIndex;
+            DetailMaterialShaderData& shaderData = m_detailMaterialShaderData.GetElement(bufferIndex);
+
+            for (uint16_t imageIndex :
+                {
+                    shaderData.m_colorImageIndex,
+                    shaderData.m_normalImageIndex,
+                    shaderData.m_roughnessImageIndex,
+                    shaderData.m_metalnessImageIndex,
+                    shaderData.m_specularF0ImageIndex,
+                    shaderData.m_occlusionImageIndex,
+                    shaderData.m_heightImageIndex
+                })
+            {
+                if (imageIndex != InvalidImageIndex)
+                {
+                    m_bindlessImageHandler->RemoveBindlessImage(imageIndex);
+                }
+            }
+
+            m_detailMaterialShaderData.Release(bufferIndex);
+            m_detailMaterials.RemoveIndex(detailMaterialId);
+
+            m_detailMaterialBufferNeedsUpdate = true;
+        }
+    }
+    
+    uint16_t TerrainDetailMaterialManager::CreateOrUpdateDetailMaterial(MaterialInstance material)
+    {
+        static constexpr uint16_t InvalidDetailMaterial = 0xFFFF;
+        uint16_t detailMaterialId = InvalidDetailMaterial;
+
+        for (auto& detailMaterialData : m_detailMaterials.GetDataVector())
+        {
+            if (detailMaterialData.m_assetId == material->GetAssetId())
+            {
+                detailMaterialId = m_detailMaterials.GetIndexForData(&detailMaterialData);
+                UpdateDetailMaterialData(detailMaterialId, material);
+                break;
+            }
+        }
+
+        AZ_Assert(m_detailMaterialShaderData.GetSize() < 0xFF, "Only 255 detail materials supported.");
+
+        if (detailMaterialId == InvalidDetailMaterial && m_detailMaterialShaderData.GetSize() < 0xFF)
+        {
+            detailMaterialId = m_detailMaterials.GetFreeSlotIndex();
+            auto& detailMaterialData = m_detailMaterials.GetData(detailMaterialId);
+            detailMaterialData.m_detailMaterialBufferIndex = aznumeric_cast<uint16_t>(m_detailMaterialShaderData.Reserve());
+            UpdateDetailMaterialData(detailMaterialId, material);
+        }
+        return detailMaterialId;
+    }
+    
+    void TerrainDetailMaterialManager::UpdateDetailMaterialData(uint16_t detailMaterialIndex, MaterialInstance material)
+    {
+        DetailMaterialData& materialData = m_detailMaterials.GetData(detailMaterialIndex);
+        DetailMaterialShaderData& shaderData = m_detailMaterialShaderData.GetElement(materialData.m_detailMaterialBufferIndex);
+
+        if (materialData.m_materialChangeId == material->GetCurrentChangeId())
+        {
+            return; // material hasn't changed, nothing to do
+        }
+
+        materialData.m_materialChangeId = material->GetCurrentChangeId();
+        materialData.m_assetId = material->GetAssetId();
+            
+        DetailTextureFlags& flags = shaderData.m_flags;
+            
+        auto getIndex = [&](const char* const indexName) -> AZ::RPI::MaterialPropertyIndex
+        {
+            const AZ::RPI::MaterialPropertyIndex index = material->FindPropertyIndex(AZ::Name(indexName));
+            AZ_Warning(TerrainDetailMaterialManagerName, index.IsValid(), "Failed to find shader input constant %s.", indexName);
+            return index;
+        };
+
+        auto applyProperty = [&](const char* const indexName, auto& ref) -> void
+        {
+            const auto index = getIndex(indexName);
+            if (index.IsValid())
+            {
+                // GetValue<T>() expects the actaul type, not a reference type, so the reference needs to be removed.
+                using TypeRefRemoved = AZStd::remove_cvref_t<decltype(ref)>;
+                ref = material->GetPropertyValue(index).GetValue<TypeRefRemoved>();
+            }
+        };
+
+        auto applyImage = [&](const char* const indexName, AZ::Data::Instance<AZ::RPI::Image>& ref, const char* const usingFlagName, DetailTextureFlags flagToSet, uint16_t& imageIndex) -> void
+        {
+            // Determine if an image exists and if its using flag allows it to be used.
+            const auto index = getIndex(indexName);
+            const auto useTextureIndex = getIndex(usingFlagName);
+            bool useTextureValue = true;
+            if (useTextureIndex.IsValid())
+            {
+                useTextureValue = material->GetPropertyValue(useTextureIndex).GetValue<bool>();
+            }
+            if (index.IsValid() && useTextureValue)
+            {
+                ref = material->GetPropertyValue(index).GetValue<AZ::Data::Instance<AZ::RPI::Image>>();
+            }
+            useTextureValue = useTextureValue && ref;
+            flags = DetailTextureFlags(useTextureValue ? (flags | flagToSet) : (flags & ~flagToSet));
+
+            // Update queues to add/remove textures depending on if the image is used
+            if (ref)
+            {
+                if (imageIndex == InvalidImageIndex)
+                {
+                    imageIndex = m_bindlessImageHandler->AppendBindlessImage(ref->GetImageView());
+                }
+                else
+                {
+                    m_bindlessImageHandler->UpdateBindlessImage(imageIndex, ref->GetImageView());
+                }
+            }
+            else if (imageIndex != InvalidImageIndex)
+            {
+                m_bindlessImageHandler->RemoveBindlessImage(imageIndex);
+                imageIndex = InvalidImageIndex;
+            }
+        };
+            
+        auto applyFlag = [&](const char* const indexName, DetailTextureFlags flagToSet) -> void
+        {
+            const auto index = getIndex(indexName);
+            if (index.IsValid())
+            {
+                bool flagValue = material->GetPropertyValue(index).GetValue<bool>();
+                flags = DetailTextureFlags(flagValue ? flags | flagToSet : flags);
+            }
+        };
+
+        auto getEnumName = [&](const char* const indexName) -> const AZStd::string_view
+        {
+            const auto index = getIndex(indexName);
+            if (index.IsValid())
+            {
+                uint32_t enumIndex = material->GetPropertyValue(index).GetValue<uint32_t>();
+                const AZ::Name& enumName = material->GetMaterialPropertiesLayout()->GetPropertyDescriptor(index)->GetEnumName(enumIndex);
+                return enumName.GetStringView();
+            }
+            return "";
+        };
+
+        using namespace DetailMaterialInputs;
+        applyImage(BaseColorMap, materialData.m_colorImage, BaseColorUseTexture, DetailTextureFlags::UseTextureBaseColor, shaderData.m_colorImageIndex);
+        applyProperty(BaseColorFactor, shaderData.m_baseColorFactor);
+
+        const auto index = getIndex(BaseColorColor);
+        if (index.IsValid())
+        {
+            AZ::Color baseColor = material->GetPropertyValue(index).GetValue<AZ::Color>();
+            shaderData.m_baseColorRed = baseColor.GetR();
+            shaderData.m_baseColorGreen = baseColor.GetG();
+            shaderData.m_baseColorBlue = baseColor.GetB();
+        }
+
+        const AZStd::string_view& blendModeString = getEnumName(BaseColorBlendMode);
+        if (blendModeString == "Multiply")
+        {
+            flags = DetailTextureFlags(flags | DetailTextureFlags::BlendModeMultiply);
+        }
+        else if (blendModeString == "LinearLight")
+        {
+            flags = DetailTextureFlags(flags | DetailTextureFlags::BlendModeLinearLight);
+        }
+        else if (blendModeString == "Lerp")
+        {
+            flags = DetailTextureFlags(flags | DetailTextureFlags::BlendModeLerp);
+        }
+        else if (blendModeString == "Overlay")
+        {
+            flags = DetailTextureFlags(flags | DetailTextureFlags::BlendModeOverlay);
+        }
+            
+        applyImage(MetallicMap, materialData.m_metalnessImage, MetallicUseTexture, DetailTextureFlags::UseTextureMetallic, shaderData.m_metalnessImageIndex);
+        applyProperty(MetallicFactor, shaderData.m_metalFactor);
+            
+        applyImage(RoughnessMap, materialData.m_roughnessImage, RoughnessUseTexture, DetailTextureFlags::UseTextureRoughness, shaderData.m_roughnessImageIndex);
+
+        if ((flags & DetailTextureFlags::UseTextureRoughness) > 0)
+        {
+            float lowerBound = 0.0;
+            float upperBound = 1.0;
+            applyProperty(RoughnessLowerBound, lowerBound);
+            applyProperty(RoughnessUpperBound, upperBound);
+            shaderData.m_roughnessBias = lowerBound;
+            shaderData.m_roughnessScale = upperBound - lowerBound;
+        }
+        else
+        {
+            shaderData.m_roughnessBias = 0.0;
+            applyProperty(RoughnessFactor, shaderData.m_roughnessScale);
+        }
+            
+        applyImage(SpecularF0Map, materialData.m_specularF0Image, SpecularF0UseTexture, DetailTextureFlags::UseTextureSpecularF0, shaderData.m_specularF0ImageIndex);
+        applyProperty(SpecularF0Factor, shaderData.m_specularF0Factor);
+            
+        applyImage(NormalMap, materialData.m_normalImage, NormalUseTexture, DetailTextureFlags::UseTextureNormal, shaderData.m_normalImageIndex);
+        applyProperty(NormalFactor, shaderData.m_normalFactor);
+        applyFlag(NormalFlipX, DetailTextureFlags::FlipNormalX);
+        applyFlag(NormalFlipY, DetailTextureFlags::FlipNormalY);
+            
+        applyImage(DiffuseOcclusionMap, materialData.m_occlusionImage, DiffuseOcclusionUseTexture, DetailTextureFlags::UseTextureOcclusion, shaderData.m_occlusionImageIndex);
+        applyProperty(DiffuseOcclusionFactor, shaderData.m_occlusionFactor);
+            
+        applyImage(HeightMap, materialData.m_heightImage, HeightUseTexture, DetailTextureFlags::UseTextureHeight, shaderData.m_heightImageIndex);
+        applyProperty(HeightFactor, shaderData.m_heightFactor);
+        applyProperty(HeightOffset, shaderData.m_heightOffset);
+        applyProperty(HeightBlendFactor, shaderData.m_heightBlendFactor);
+
+        m_detailMaterialBufferNeedsUpdate = true;
+    }
+    
+    void TerrainDetailMaterialManager::CheckUpdateDetailTexture(const Aabb2i& newBounds, const Vector2i& newCenter)
+    {
+        if (r_terrainDebugDetailImageUpdates)
+        {
+            AZ_Printf("TerrainDetailMaterialManager", "Old Bounds: m(%i, %i)M(%i, %i) New Bounds: m(%i, %i)M(%i, %i)",
+                m_detailTextureBounds.m_min.m_x, m_detailTextureBounds.m_min.m_y, m_detailTextureBounds.m_max.m_x, m_detailTextureBounds.m_max.m_y,
+                newBounds.m_min.m_x, newBounds.m_min.m_y, newBounds.m_max.m_x, newBounds.m_max.m_y
+            );
+        }
+        if (!m_detailTextureImage)
+        {
+            // If the m_detailTextureImage doesn't exist, create it and populate the entire texture
+
+            const AZ::Data::Instance<AZ::RPI::AttachmentImagePool> imagePool = AZ::RPI::ImageSystemInterface::Get()->GetSystemAttachmentPool();
+            AZ::RHI::ImageDescriptor imageDescriptor = AZ::RHI::ImageDescriptor::Create2D(
+                AZ::RHI::ImageBindFlags::ShaderRead, DetailTextureSize, DetailTextureSize, AZ::RHI::Format::R8G8B8A8_UINT
+            );
+            const AZ::Name TerrainDetailName = AZ::Name(TerrainDetailChars);
+            m_detailTextureImage = AZ::RPI::AttachmentImage::Create(*imagePool.get(), imageDescriptor, TerrainDetailName, nullptr, nullptr);
+            AZ_Error(TerrainDetailMaterialManagerName, m_detailTextureImage, "Failed to initialize the detail texture image.");
+            
+            UpdateDetailTexture(newBounds, newBounds, newCenter);
+        }
+        else
+        {
+            // If the new bounds of the detail texture are different than the old bounds, then the edges of the texture need to be updated.
+
+            int32_t offsetX = m_detailTextureBounds.m_min.m_x - newBounds.m_min.m_x;
+
+            // Horizontal edge update
+            if (newBounds.m_min.m_x != m_detailTextureBounds.m_min.m_x)
+            {
+                Aabb2i updateBounds;
+                if (newBounds.m_min.m_x < m_detailTextureBounds.m_min.m_x)
+                {
+                    updateBounds.m_min.m_x = newBounds.m_min.m_x;
+                    updateBounds.m_max.m_x = m_detailTextureBounds.m_min.m_x;
+                }
+                else
+                {
+                    updateBounds.m_min.m_x = m_detailTextureBounds.m_max.m_x;
+                    updateBounds.m_max.m_x = newBounds.m_max.m_x;
+                }
+                updateBounds.m_min.m_y = newBounds.m_min.m_y;
+                updateBounds.m_max.m_y = newBounds.m_max.m_y;
+                
+                if (r_terrainDebugDetailImageUpdates)
+                {
+                    AZ_Printf("TerrainDetailMaterialManager", "Updating horizontal edge: m(%i, %i)M(%i, %i)",
+                        updateBounds.m_min.m_x, updateBounds.m_min.m_y, updateBounds.m_max.m_x, updateBounds.m_max.m_y);
+                }
+                UpdateDetailTexture(updateBounds, newBounds, newCenter);
+            }
+
+            // Vertical edge update
+            if (newBounds.m_min.m_y != m_detailTextureBounds.m_min.m_y)
+            {
+                Aabb2i updateBounds;
+                // Don't update areas that have already been updated in the horizontal update.
+                updateBounds.m_min.m_x = newBounds.m_min.m_x + AZ::GetMax(0, offsetX);
+                updateBounds.m_max.m_x = newBounds.m_max.m_x + AZ::GetMin(0, offsetX);
+                if (newBounds.m_min.m_y < m_detailTextureBounds.m_min.m_y)
+                {
+                    updateBounds.m_min.m_y = newBounds.m_min.m_y;
+                    updateBounds.m_max.m_y = m_detailTextureBounds.m_min.m_y;
+                }
+                else
+                {
+                    updateBounds.m_min.m_y = m_detailTextureBounds.m_max.m_y;
+                    updateBounds.m_max.m_y = newBounds.m_max.m_y;
+                }
+                
+                if (r_terrainDebugDetailImageUpdates)
+                {
+                    AZ_Printf("TerrainDetailMaterialManager", "Updating vertical edge: m(%i, %i)M(%i, %i)",
+                        updateBounds.m_min.m_x, updateBounds.m_min.m_y, updateBounds.m_max.m_x, updateBounds.m_max.m_y);
+                }
+                UpdateDetailTexture(updateBounds, newBounds, newCenter);
+            }
+
+            if (m_dirtyDetailRegion.IsValid())
+            {
+                if (r_terrainDebugDetailImageUpdates)
+                {
+                    AZ_Printf("TerrainDetailMaterialManager", "m_dirtyDetailRegion: m(%f, %f)M(%f, %f)",
+                        m_dirtyDetailRegion.GetMin().GetX(), m_dirtyDetailRegion.GetMin().GetY(), m_dirtyDetailRegion.GetMax().GetX(), m_dirtyDetailRegion.GetMax().GetY());
+                }
+                // If any regions are marked as dirty, then they should be updated.
+
+                AZ::Vector3 currentMin = AZ::Vector3(newBounds.m_min.m_x * DetailTextureScale, newBounds.m_min.m_y * DetailTextureScale, -0.5f);
+                AZ::Vector3 currentMax = AZ::Vector3(newBounds.m_max.m_x * DetailTextureScale, newBounds.m_max.m_y * DetailTextureScale, 0.5f);
+                AZ::Aabb detailTextureCoverage = AZ::Aabb::CreateFromMinMax(currentMin, currentMax);
+                AZ::Vector3 previousMin = AZ::Vector3(m_detailTextureBounds.m_min.m_x * DetailTextureScale, m_detailTextureBounds.m_min.m_y * DetailTextureScale, -0.5f);
+                AZ::Vector3 previousMax = AZ::Vector3(m_detailTextureBounds.m_max.m_x * DetailTextureScale, m_detailTextureBounds.m_max.m_y * DetailTextureScale, 0.5f);
+                AZ::Aabb previousCoverage = AZ::Aabb::CreateFromMinMax(previousMin, previousMax);
+
+                // Area of texture not already updated by camera movement above.
+                AZ::Aabb clampedCoverage = previousCoverage.GetClamped(detailTextureCoverage);
+
+                // Clamp the dirty region to the area of the detail texture that is visible and not already updated.
+                clampedCoverage.Clamp(m_dirtyDetailRegion);
+
+                if (clampedCoverage.IsValid())
+                {
+                    Aabb2i updateBounds;
+                    updateBounds.m_min.m_x = aznumeric_cast<int32_t>(AZStd::roundf(clampedCoverage.GetMin().GetX() / DetailTextureScale));
+                    updateBounds.m_min.m_y = aznumeric_cast<int32_t>(AZStd::roundf(clampedCoverage.GetMin().GetY() / DetailTextureScale));
+                    updateBounds.m_max.m_x = aznumeric_cast<int32_t>(AZStd::roundf(clampedCoverage.GetMax().GetX() / DetailTextureScale));
+                    updateBounds.m_max.m_y = aznumeric_cast<int32_t>(AZStd::roundf(clampedCoverage.GetMax().GetY() / DetailTextureScale));
+                    if (updateBounds.m_min.m_x < updateBounds.m_max.m_x && updateBounds.m_min.m_y < updateBounds.m_max.m_y)
+                    {
+                        
+                        if (r_terrainDebugDetailImageUpdates)
+                        {
+                            AZ_Printf("TerrainDetailMaterialManager", "Updating dirty region: m(%i, %i)M(%i, %i)",
+                                updateBounds.m_min.m_x, updateBounds.m_min.m_y, updateBounds.m_max.m_x, updateBounds.m_max.m_y);
+                        }
+                        UpdateDetailTexture(updateBounds, newBounds, newCenter);
+                    }
+                }
+            }
+        }
+    }
+    
+    void TerrainDetailMaterialManager::UpdateDetailTexture(const Aabb2i& updateArea, const Aabb2i& textureBounds, const Vector2i& centerPixel)
+    {
+        if (!m_detailTextureImage)
+        {
+            return;
+        }
+
+        struct DetailMaterialPixel
+        {
+            uint8_t m_material1{ 255 };
+            uint8_t m_material2{ 255 };
+            uint8_t m_blend{ 0 }; // 0 = full weight on material1, 255 = full weight on material2
+            uint8_t m_padding{ 0 };
+        };
+
+        // Because the center of the detail texture may be offset, each update area may actually need to be split into
+        // up to 4 separate update areas in each sector of the quadrant.
+        AZStd::array<Aabb2i, 4> textureSpaceAreas;
+        AZStd::array<Aabb2i, 4> scaledWorldSpaceAreas;
+        uint8_t updateAreaCount = CalculateUpdateRegions(updateArea, textureBounds, centerPixel, textureSpaceAreas, scaledWorldSpaceAreas);
+
+        if (updateAreaCount > 0)
+        {
+            m_detailImageNeedsUpdate = true;
+        }
+
+        // Pull the data for each area updated and use it to construct an update for the detail material id texture.
+        for (uint8_t i = 0; i < updateAreaCount; ++i)
+        {
+            const Aabb2i& quadrantTextureArea = textureSpaceAreas[i];
+            const Aabb2i& quadrantWorldArea = scaledWorldSpaceAreas[i];
+
+            AZStd::vector<DetailMaterialPixel> pixels;
+            pixels.resize((quadrantWorldArea.m_max.m_x - quadrantWorldArea.m_min.m_x) * (quadrantWorldArea.m_max.m_y - quadrantWorldArea.m_min.m_y));
+            uint32_t index = 0;
+
+            for (int yPos = quadrantWorldArea.m_min.m_y; yPos < quadrantWorldArea.m_max.m_y; ++yPos)
+            {
+                for (int xPos = quadrantWorldArea.m_min.m_x; xPos < quadrantWorldArea.m_max.m_x; ++xPos)
+                {
+                    AZ::Vector2 position = AZ::Vector2(xPos * DetailTextureScale, yPos * DetailTextureScale);
+                    AzFramework::SurfaceData::SurfaceTagWeightList surfaceWeights;
+                    AzFramework::Terrain::TerrainDataRequestBus::Broadcast(&AzFramework::Terrain::TerrainDataRequests::GetSurfaceWeightsFromVector2, position, surfaceWeights, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, nullptr);
+
+                    // Store the top two surface weights in the texture with m_blend storing the relative weight.
+                    bool isFirstMaterial = true;
+                    float firstWeight = 0.0f;
+                    for (const auto& surfaceTagWeight : surfaceWeights)
+                    {
+                        if (surfaceTagWeight.m_weight > 0.0f)
+                        {
+                            AZ::Crc32 surfaceType = surfaceTagWeight.m_surfaceType;
+                            uint16_t materialId = GetDetailMaterialForSurfaceTypeAndPosition(surfaceType, position);
+                            if (materialId != m_detailMaterials.NoFreeSlot && materialId < 255)
+                            {
+                                if (isFirstMaterial)
+                                {
+                                    pixels.at(index).m_material1 = aznumeric_cast<uint8_t>(materialId);
+                                    firstWeight = surfaceTagWeight.m_weight;
+                                    // m_blend only needs to be calculated is material 2 is found, otherwise the initial value of 0 is correct.
+                                    isFirstMaterial = false;
+                                }
+                                else
+                                {
+                                    pixels.at(index).m_material2 = aznumeric_cast<uint8_t>(materialId);
+                                    float totalWeight = firstWeight + surfaceTagWeight.m_weight;
+                                    float blendWeight = 1.0f - (firstWeight / totalWeight);
+                                    pixels.at(index).m_blend = aznumeric_cast<uint8_t>(AZStd::round(blendWeight * 255.0f));
+                                    break;
+                                }
+                            }
+                        }
+                        else
+                        {
+                            break; // since the list is ordered, no other materials are in the list with positive weights.
+                        }
+                    }
+                    ++index;
+                }
+            }
+
+            const int32_t left = quadrantTextureArea.m_min.m_x;
+            const int32_t top = quadrantTextureArea.m_min.m_y;
+            const int32_t width = quadrantTextureArea.m_max.m_x - quadrantTextureArea.m_min.m_x;
+            const int32_t height = quadrantTextureArea.m_max.m_y - quadrantTextureArea.m_min.m_y;
+
+            AZ::RHI::ImageUpdateRequest imageUpdateRequest;
+            imageUpdateRequest.m_imageSubresourcePixelOffset.m_left = aznumeric_cast<uint32_t>(left);
+            imageUpdateRequest.m_imageSubresourcePixelOffset.m_top = aznumeric_cast<uint32_t>(top);
+            imageUpdateRequest.m_sourceSubresourceLayout.m_bytesPerRow = width * sizeof(DetailMaterialPixel);
+            imageUpdateRequest.m_sourceSubresourceLayout.m_bytesPerImage = width * height * sizeof(DetailMaterialPixel);
+            imageUpdateRequest.m_sourceSubresourceLayout.m_rowCount = height;
+            imageUpdateRequest.m_sourceSubresourceLayout.m_size.m_width = width;
+            imageUpdateRequest.m_sourceSubresourceLayout.m_size.m_height = height;
+            imageUpdateRequest.m_sourceSubresourceLayout.m_size.m_depth = 1;
+            imageUpdateRequest.m_sourceData = pixels.data();
+            imageUpdateRequest.m_image = m_detailTextureImage->GetRHIImage();
+
+            m_detailTextureImage->UpdateImageContents(imageUpdateRequest);
+        }
+    }
+    
+    uint8_t TerrainDetailMaterialManager::CalculateUpdateRegions(const Aabb2i& updateArea, const Aabb2i& textureBounds, const Vector2i& centerPixel,
+        AZStd::array<Aabb2i, 4>& textureSpaceAreas, AZStd::array<Aabb2i, 4>& scaledWorldSpaceAreas)
+    {
+        Vector2i centerOffset = { centerPixel.m_x - DetailTextureSizeHalf, centerPixel.m_y - DetailTextureSizeHalf };
+
+        int32_t quadrantXOffset = centerPixel.m_x < DetailTextureSizeHalf ? DetailTextureSize : -DetailTextureSize;
+        int32_t quadrantYOffset = centerPixel.m_y < DetailTextureSizeHalf ? DetailTextureSize : -DetailTextureSize;
+
+        uint8_t numQuadrants = 0;
+
+        // For each of the 4 quadrants:
+        auto calculateQuadrant = [&](Vector2i quadrantOffset)
+        {
+            Aabb2i offsetUpdateArea = updateArea + centerOffset + quadrantOffset;
+            Aabb2i updateSectionBounds = textureBounds.GetClamped(offsetUpdateArea);
+            if (updateSectionBounds.IsValid())
+            {
+                textureSpaceAreas[numQuadrants] = updateSectionBounds - textureBounds.m_min;
+                scaledWorldSpaceAreas[numQuadrants] = updateSectionBounds - centerOffset - quadrantOffset;
+                ++numQuadrants;
+            }
+        };
+
+        calculateQuadrant({ 0, 0 });
+        calculateQuadrant({ quadrantXOffset, 0 });
+        calculateQuadrant({ 0, quadrantYOffset });
+        calculateQuadrant({ quadrantXOffset, quadrantYOffset });
+
+        return numQuadrants;
+    }
+
+    uint16_t TerrainDetailMaterialManager::GetDetailMaterialForSurfaceTypeAndPosition(AZ::Crc32 surfaceType, const AZ::Vector2& position)
+    {
+        for (const auto& materialRegion : m_detailMaterialRegions.GetDataVector())
+        {
+            if (materialRegion.m_region.Contains(AZ::Vector3(position.GetX(), position.GetY(), 0.0f)))
+            {
+                for (const auto& materialSurface : materialRegion.m_materialsForSurfaces)
+                {
+                    if (materialSurface.m_surfaceTag == surfaceType)
+                    {
+                        return m_detailMaterials.GetData(materialSurface.m_detailMaterialId).m_detailMaterialBufferIndex;
+                    }
+                }
+            }
+        }
+        return m_detailMaterials.NoFreeSlot;
+    }
+
+    auto TerrainDetailMaterialManager::FindByEntityId(AZ::EntityId entityId, AZ::Render::IndexedDataVector<DetailMaterialListRegion>& container)
+        -> DetailMaterialListRegion*
+    {
+        for (DetailMaterialListRegion& data : container.GetDataVector())
+        {
+            if (data.m_entityId == entityId)
+            {
+                return &data;
+            }
+        }
+        return nullptr;
+    }
+    
+    auto TerrainDetailMaterialManager::FindOrCreateByEntityId(AZ::EntityId entityId, AZ::Render::IndexedDataVector<DetailMaterialListRegion>& container)
+        -> DetailMaterialListRegion&
+    {
+        DetailMaterialListRegion* dataPtr = FindByEntityId(entityId, container);
+        if (dataPtr != nullptr)
+        {
+            return *dataPtr;
+        }
+
+        const uint16_t slotId = container.GetFreeSlotIndex();
+        AZ_Assert(slotId != AZ::Render::IndexedDataVector<TerrainDetailMaterialManager>::NoFreeSlot, "Ran out of indices");
+
+        DetailMaterialListRegion& data = container.GetData(slotId);
+        data.m_entityId = entityId;
+        return data;
+    }
+    
+    void TerrainDetailMaterialManager::RemoveByEntityId(AZ::EntityId entityId, AZ::Render::IndexedDataVector<DetailMaterialListRegion>& container)
+    {
+        for (DetailMaterialListRegion& data : container.GetDataVector())
+        {
+            if (data.m_entityId == entityId)
+            {
+                container.RemoveData(&data);
+                return;
+            }
+        }
+        AZ_Assert(false, "Entity Id not found in container.")
+    }
+    
+}

+ 226 - 0
Gems/Terrain/Code/Source/TerrainRenderer/TerrainDetailMaterialManager.h

@@ -0,0 +1,226 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <AzCore/base.h>
+#include <AzCore/Math/Aabb.h>
+#include <AzCore/std/containers/array.h>
+
+#include <AzFramework/Terrain/TerrainDataRequestBus.h>
+
+#include <TerrainRenderer/Aabb2i.h>
+#include <TerrainRenderer/BindlessImageArrayHandler.h>
+#include <TerrainRenderer/TerrainAreaMaterialRequestBus.h>
+#include <TerrainRenderer/Vector2i.h>
+
+#include <Atom/RPI.Public/Material/Material.h>
+#include <Atom/RPI.Public/Image/AttachmentImage.h>
+#include <Atom/RPI.Reflect/Image/Image.h>
+#include <Atom/Feature/Utils/IndexedDataVector.h>
+#include <Atom/Feature/Utils/SparseVector.h>
+#include <Atom/Feature/Utils/GpuBufferHandler.h>
+
+namespace Terrain
+{
+    class TerrainDetailMaterialManager
+        : private AzFramework::Terrain::TerrainDataNotificationBus::Handler
+        , private TerrainAreaMaterialNotificationBus::Handler
+    {
+    public:
+
+        AZ_RTTI(TerrainDetailMaterialManager, "{3CBAF88F-E3B1-43B8-97A5-999133188BCC}");
+        AZ_DISABLE_COPY_MOVE(TerrainDetailMaterialManager);
+
+        TerrainDetailMaterialManager() = default;
+        ~TerrainDetailMaterialManager() = default;
+        
+        void Initialize(
+            const AZStd::shared_ptr<AZ::Render::BindlessImageArrayHandler>& bindlessImageHandler,
+            AZ::Data::Instance<AZ::RPI::ShaderResourceGroup>& terrainSrg);
+        bool IsInitialized() const;
+        void Reset();
+        bool UpdateSrgIndices(AZ::Data::Instance<AZ::RPI::ShaderResourceGroup>& srg);
+
+        void Update(const AZ::Vector3& cameraPosition, AZ::Data::Instance<AZ::RPI::ShaderResourceGroup>& terrainSrg);
+
+    private:
+        
+        using MaterialInstance = AZ::Data::Instance<AZ::RPI::Material>;
+        static constexpr auto InvalidImageIndex = AZ::Render::BindlessImageArrayHandler::InvalidImageIndex;
+
+        enum DetailTextureFlags : uint32_t
+        {
+            UseTextureBaseColor =  0b0000'0000'0000'0000'0000'0000'0000'0001,
+            UseTextureNormal =     0b0000'0000'0000'0000'0000'0000'0000'0010,
+            UseTextureMetallic =   0b0000'0000'0000'0000'0000'0000'0000'0100,
+            UseTextureRoughness =  0b0000'0000'0000'0000'0000'0000'0000'1000,
+            UseTextureOcclusion =  0b0000'0000'0000'0000'0000'0000'0001'0000,
+            UseTextureHeight =     0b0000'0000'0000'0000'0000'0000'0010'0000,
+            UseTextureSpecularF0 = 0b0000'0000'0000'0000'0000'0000'0100'0000,
+
+            FlipNormalX =          0b0000'0000'0000'0001'0000'0000'0000'0000,
+            FlipNormalY =          0b0000'0000'0000'0010'0000'0000'0000'0000,
+
+            BlendModeMask =        0b0000'0000'0000'1100'0000'0000'0000'0000,
+            BlendModeLerp =        0b0000'0000'0000'0000'0000'0000'0000'0000,
+            BlendModeLinearLight = 0b0000'0000'0000'0100'0000'0000'0000'0000,
+            BlendModeMultiply =    0b0000'0000'0000'1000'0000'0000'0000'0000,
+            BlendModeOverlay =     0b0000'0000'0000'1100'0000'0000'0000'0000,
+        };
+        
+        struct DetailMaterialShaderData
+        {
+            // Uv
+            AZStd::array<float, 12> m_uvTransform
+            {
+                1.0, 0.0, 0.0, 0.0,
+                0.0, 1.0, 0.0, 0.0,
+                0.0, 0.0, 1.0, 0.0,
+            };
+
+            float m_baseColorRed{ 1.0f };
+            float m_baseColorGreen{ 1.0f };
+            float m_baseColorBlue{ 1.0f };
+
+            // Factor / Scale / Bias for input textures
+            float m_baseColorFactor{ 1.0f };
+
+            float m_normalFactor{ 1.0f };
+            float m_metalFactor{ 1.0f };
+            float m_roughnessScale{ 1.0f };
+            float m_roughnessBias{ 0.0f };
+
+            float m_specularF0Factor{ 1.0f };
+            float m_occlusionFactor{ 1.0f };
+            float m_heightFactor{ 1.0f };
+            float m_heightOffset{ 0.0f };
+
+            float m_heightBlendFactor{ 0.5f };
+
+            // Flags
+            DetailTextureFlags m_flags{ 0 };
+
+            // Image indices
+            uint16_t m_colorImageIndex{ InvalidImageIndex };
+            uint16_t m_normalImageIndex{ InvalidImageIndex };
+            uint16_t m_roughnessImageIndex{ InvalidImageIndex };
+            uint16_t m_metalnessImageIndex{ InvalidImageIndex };
+
+            uint16_t m_specularF0ImageIndex{ InvalidImageIndex };
+            uint16_t m_occlusionImageIndex{ InvalidImageIndex };
+            uint16_t m_heightImageIndex{ InvalidImageIndex };
+
+            // 16 byte aligned
+            uint16_t m_padding1;
+            uint32_t m_padding2;
+            uint32_t m_padding3;
+        };
+        static_assert(sizeof(DetailMaterialShaderData) % 16 == 0, "DetailMaterialShaderData must be 16 byte aligned.");
+
+        struct DetailMaterialData
+        {
+            AZ::Data::AssetId m_assetId;
+            AZ::RPI::Material::ChangeId m_materialChangeId{AZ::RPI::Material::DEFAULT_CHANGE_ID};
+            uint32_t refCount = 0;
+            uint16_t m_detailMaterialBufferIndex{ 0xFFFF };
+
+            AZ::Data::Instance<AZ::RPI::Image> m_colorImage;
+            AZ::Data::Instance<AZ::RPI::Image> m_normalImage;
+            AZ::Data::Instance<AZ::RPI::Image> m_roughnessImage;
+            AZ::Data::Instance<AZ::RPI::Image> m_metalnessImage;
+            AZ::Data::Instance<AZ::RPI::Image> m_specularF0Image;
+            AZ::Data::Instance<AZ::RPI::Image> m_occlusionImage;
+            AZ::Data::Instance<AZ::RPI::Image> m_heightImage;
+        };
+
+        struct DetailMaterialSurface
+        {
+            AZ::Crc32 m_surfaceTag;
+            uint16_t m_detailMaterialId;
+        };
+
+        struct DetailMaterialListRegion
+        {
+            AZ::EntityId m_entityId;
+            AZ::Aabb m_region{AZ::Aabb::CreateNull()};
+            AZStd::vector<DetailMaterialSurface> m_materialsForSurfaces;
+        };
+        
+        // System-level parameters
+        static constexpr int32_t DetailTextureSize{ 1024 };
+        static constexpr int32_t DetailTextureSizeHalf{ DetailTextureSize / 2 };
+        static constexpr float DetailTextureScale{ 0.5f };
+        
+        // AzFramework::Terrain::TerrainDataNotificationBus overrides...
+        void OnTerrainDataChanged(const AZ::Aabb& dirtyRegion, TerrainDataChangedMask dataChangedMask) override;
+        
+        // TerrainAreaMaterialNotificationBus overrides...
+        void OnTerrainSurfaceMaterialMappingCreated(AZ::EntityId entityId, SurfaceData::SurfaceTag surfaceTag, MaterialInstance material) override;
+        void OnTerrainSurfaceMaterialMappingDestroyed(AZ::EntityId entityId, SurfaceData::SurfaceTag surfaceTag) override;
+        void OnTerrainSurfaceMaterialMappingChanged(AZ::EntityId entityId, SurfaceData::SurfaceTag surfaceTag, MaterialInstance material) override;
+        void OnTerrainSurfaceMaterialMappingRegionChanged(AZ::EntityId entityId, const AZ::Aabb& oldRegion, const AZ::Aabb& newRegion) override;
+
+        //! Removes all images from all detail materials from the bindless image array
+        void RemoveAllImages();
+
+        //! Creates or updates an existing detail material with settings from a material instance
+        uint16_t CreateOrUpdateDetailMaterial(MaterialInstance material);
+
+        //! Decrements the ref count on a detail material and removes it if it reaches 0
+        void CheckDetailMaterialForDeletion(uint16_t detailMaterialId);
+
+        //! Updates a specific detail material with settings from a material instance
+        void UpdateDetailMaterialData(uint16_t detailMaterialIndex, MaterialInstance material);
+
+        //! Checks to see if the detail material id texture needs to update based on new center and bounds. Any
+        //! required updates are then executed.
+        void CheckUpdateDetailTexture(const Aabb2i& newBounds, const Vector2i& newCenter);
+
+        //! Updates the detail texture in a given area
+        void UpdateDetailTexture(const Aabb2i& updateArea, const Aabb2i& textureBounds, const Vector2i& centerPixel);
+
+        //! Finds the detail material Id for a surface type and position
+        uint16_t GetDetailMaterialForSurfaceTypeAndPosition(AZ::Crc32 surfaceType, const AZ::Vector2& position);
+
+        //! Calculates which regions of the detail material id texture need to be updated based on the update area. Since
+        //! the "center" of the detail material id texture can move, a single update region in contiguous world space may
+        //! map to up to 4 different areas on teh detail material id texture.
+        uint8_t CalculateUpdateRegions(const Aabb2i& updateArea, const Aabb2i& textureBounds, const Vector2i& centerPixel,
+            AZStd::array<Aabb2i, 4>& textureSpaceAreas, AZStd::array<Aabb2i, 4>& scaledWorldSpaceAreas);
+
+        DetailMaterialListRegion* FindByEntityId(AZ::EntityId entityId, AZ::Render::IndexedDataVector<DetailMaterialListRegion>& container);
+        DetailMaterialListRegion& FindOrCreateByEntityId(AZ::EntityId entityId, AZ::Render::IndexedDataVector<DetailMaterialListRegion>& container);
+        void RemoveByEntityId(AZ::EntityId entityId, AZ::Render::IndexedDataVector<DetailMaterialListRegion>& container);
+        
+        AZStd::shared_ptr<AZ::Render::BindlessImageArrayHandler> m_bindlessImageHandler;
+        
+        AZ::Data::Instance<AZ::RPI::AttachmentImage> m_detailTextureImage;
+
+        AZ::Render::IndexedDataVector<DetailMaterialData> m_detailMaterials;
+        AZ::Render::IndexedDataVector<DetailMaterialListRegion> m_detailMaterialRegions;
+        AZ::Render::SparseVector<DetailMaterialShaderData> m_detailMaterialShaderData;
+        AZ::Render::GpuBufferHandler m_detailMaterialDataBuffer;
+        
+        AZ::Aabb m_dirtyDetailRegion{ AZ::Aabb::CreateNull() };
+        AZ::Vector3 m_previousCameraPosition = AZ::Vector3(AZStd::numeric_limits<float>::max(), 0.0, 0.0);
+        Aabb2i m_detailTextureBounds;
+        Vector2i m_detailTextureCenter;
+
+        AZ::RHI::ShaderInputImageIndex m_detailMaterialIdPropertyIndex;
+        AZ::RHI::ShaderInputBufferIndex m_detailMaterialDataIndex;
+        AZ::RHI::ShaderInputConstantIndex m_detailCenterPropertyIndex;
+        AZ::RHI::ShaderInputConstantIndex m_detailAabbPropertyIndex;
+        AZ::RHI::ShaderInputConstantIndex m_detailHalfPixelUvPropertyIndex;
+
+        bool m_isInitialized{ false };
+        bool m_detailMaterialBufferNeedsUpdate{ false };
+        bool m_detailImageNeedsUpdate{ false };
+
+    };
+}

File diff suppressed because it is too large
+ 176 - 1328
Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.cpp


+ 29 - 285
Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.h

@@ -8,20 +8,17 @@
 
 #pragma once
 
-#include <AzCore/Component/Component.h>
-
 #include <AzFramework/Terrain/TerrainDataRequestBus.h>
-#include <TerrainRenderer/TerrainMacroMaterialBus.h>
-#include <TerrainRenderer/TerrainAreaMaterialRequestBus.h>
+
+#include <TerrainRenderer/BindlessImageArrayHandler.h>
+#include <TerrainRenderer/TerrainDetailMaterialManager.h>
+#include <TerrainRenderer/TerrainMacroMaterialManager.h>
+#include <TerrainRenderer/TerrainMeshManager.h>
 
 #include <Atom/RPI.Public/FeatureProcessor.h>
 #include <Atom/RPI.Public/Image/AttachmentImage.h>
-#include <Atom/RPI.Public/MeshDrawPacket.h>
 #include <Atom/RPI.Public/Material/MaterialReloadNotificationBus.h>
 #include <Atom/RPI.Public/Shader/ShaderSystemInterface.h>
-#include <Atom/Feature/Utils/GpuBufferHandler.h>
-#include <Atom/Feature/Utils/IndexedDataVector.h>
-#include <Atom/Feature/Utils/SparseVector.h>
 
 namespace AZ::RPI
 {
@@ -30,9 +27,7 @@ namespace AZ::RPI
         class AsyncAssetLoader;
     }
     class Material;
-    class Model;
     class RenderPass;
-    class StreamingImage;
 }
 
 namespace Terrain
@@ -41,8 +36,6 @@ namespace Terrain
         : public AZ::RPI::FeatureProcessor
         , private AZ::RPI::MaterialReloadNotificationBus::Handler
         , private AzFramework::Terrain::TerrainDataNotificationBus::Handler
-        , private TerrainMacroMaterialNotificationBus::Handler
-        , private TerrainAreaMaterialNotificationBus::Handler
     {
     public:
         AZ_RTTI(TerrainFeatureProcessor, "{D7DAC1F9-4A9F-4D3C-80AE-99579BF8AB1C}", AZ::RPI::FeatureProcessor);
@@ -62,189 +55,16 @@ namespace Terrain
         void SetWorldSize(AZ::Vector2 sizeInMeters);
 
     private:
-        
-        using MaterialInstance = AZ::Data::Instance<AZ::RPI::Material>;
-        static constexpr uint32_t MaxMaterialsPerSector = 4;
-
-        enum MacroMaterialFlags
-        {
-            ColorImageUsed = 0b01,
-            NormalImageUsed = 0b10,
-        };
-
-        struct ShaderTerrainData // Must align with struct in Object Srg
-        {
-            AZStd::array<float, 2> m_uvMin{ 0.0f, 0.0f };
-            AZStd::array<float, 2> m_uvMax{ 1.0f, 1.0f };
-            AZStd::array<float, 2> m_uvStep{ 1.0f, 1.0f };
-            float m_sampleSpacing{ 1.0f };
-            float m_heightScale{ 1.0f };
-        };
-
-        struct ShaderMacroMaterialData // Must align with struct in Object Srg
-        {
-            AZStd::array<float, 2> m_uvMin{ 0.0f, 0.0f };
-            AZStd::array<float, 2> m_uvMax{ 1.0f, 1.0f };
-            float m_normalFactor{ 0.0f };
-            uint32_t m_flipNormalX{ 0 }; // bool in shader
-            uint32_t m_flipNormalY{ 0 }; // bool in shader
-            uint32_t m_mapsInUse{ 0b00 }; // 0b01 = color, 0b10 = normal
-        };
-
-        struct VertexPosition
-        {
-            float m_posx;
-            float m_posy;
-        };
-
-        struct VertexUv
-        {
-            float m_u;
-            float m_v;
-        };
-
-        struct PatchData
-        {
-            AZStd::vector<VertexPosition> m_positions;
-            AZStd::vector<VertexUv> m_uvs;
-            AZStd::vector<uint16_t> m_indices;
-        };
-        
-        struct SectorData
-        {
-            AZ::Data::Instance<AZ::RPI::ShaderResourceGroup> m_srg; // Hold on to ref so it's not dropped
-            AZ::Aabb m_aabb;
-            AZStd::fixed_vector<AZ::RPI::MeshDrawPacket, AZ::RPI::ModelLodAsset::LodCountMax> m_drawPackets;
-            AZStd::fixed_vector<uint16_t, MaxMaterialsPerSector> m_macroMaterials;
-        };
-
-        enum DetailTextureFlags : uint32_t
-        {
-            UseTextureBaseColor =  0b0000'0000'0000'0000'0000'0000'0000'0001,
-            UseTextureNormal =     0b0000'0000'0000'0000'0000'0000'0000'0010,
-            UseTextureMetallic =   0b0000'0000'0000'0000'0000'0000'0000'0100,
-            UseTextureRoughness =  0b0000'0000'0000'0000'0000'0000'0000'1000,
-            UseTextureOcclusion =  0b0000'0000'0000'0000'0000'0000'0001'0000,
-            UseTextureHeight =     0b0000'0000'0000'0000'0000'0000'0010'0000,
-            UseTextureSpecularF0 = 0b0000'0000'0000'0000'0000'0000'0100'0000,
-
-            FlipNormalX =          0b0000'0000'0000'0001'0000'0000'0000'0000,
-            FlipNormalY =          0b0000'0000'0000'0010'0000'0000'0000'0000,
-
-            BlendModeMask =        0b0000'0000'0000'1100'0000'0000'0000'0000,
-            BlendModeLerp =        0b0000'0000'0000'0000'0000'0000'0000'0000,
-            BlendModeLinearLight = 0b0000'0000'0000'0100'0000'0000'0000'0000,
-            BlendModeMultiply =    0b0000'0000'0000'1000'0000'0000'0000'0000,
-            BlendModeOverlay =     0b0000'0000'0000'1100'0000'0000'0000'0000,
-        };
-
-        static constexpr uint16_t InvalidDetailImageIndex = 0xFFFF;
-
-        struct DetailMaterialShaderData
-        {
-            // Uv
-            AZStd::array<float, 12> m_uvTransform
-            {
-                1.0, 0.0, 0.0, 0.0,
-                0.0, 1.0, 0.0, 0.0,
-                0.0, 0.0, 1.0, 0.0,
-            };
-
-            float m_baseColorRed{ 1.0f };
-            float m_baseColorGreen{ 1.0f };
-            float m_baseColorBlue{ 1.0f };
-
-            // Factor / Scale / Bias for input textures
-            float m_baseColorFactor{ 1.0f };
-
-            float m_normalFactor{ 1.0f };
-            float m_metalFactor{ 1.0f };
-            float m_roughnessScale{ 1.0f };
-            float m_roughnessBias{ 0.0f };
-
-            float m_specularF0Factor{ 1.0f };
-            float m_occlusionFactor{ 1.0f };
-            float m_heightFactor{ 1.0f };
-            float m_heightOffset{ 0.0f };
-
-            float m_heightBlendFactor{ 0.5f };
-
-            // Flags
-            DetailTextureFlags m_flags{ 0 };
-
-            // Image indices
-            uint16_t m_colorImageIndex{ InvalidDetailImageIndex };
-            uint16_t m_normalImageIndex{ InvalidDetailImageIndex };
-            uint16_t m_roughnessImageIndex{ InvalidDetailImageIndex };
-            uint16_t m_metalnessImageIndex{ InvalidDetailImageIndex };
-
-            uint16_t m_specularF0ImageIndex{ InvalidDetailImageIndex };
-            uint16_t m_occlusionImageIndex{ InvalidDetailImageIndex };
-            uint16_t m_heightImageIndex{ InvalidDetailImageIndex };
 
-            // 16 byte aligned
-            uint16_t m_padding1;
-            uint32_t m_padding2;
-            uint32_t m_padding3;
-        };
-
-        struct DetailMaterialData
-        {
-            AZ::Data::AssetId m_assetId;
-            AZ::RPI::Material::ChangeId m_materialChangeId{AZ::RPI::Material::DEFAULT_CHANGE_ID};
-            uint32_t refCount = 0;
-            uint16_t m_detailMaterialBufferIndex{ 0xFFFF };
-
-            AZ::Data::Instance<AZ::RPI::Image> m_colorImage;
-            AZ::Data::Instance<AZ::RPI::Image> m_normalImage;
-            AZ::Data::Instance<AZ::RPI::Image> m_roughnessImage;
-            AZ::Data::Instance<AZ::RPI::Image> m_metalnessImage;
-            AZ::Data::Instance<AZ::RPI::Image> m_specularF0Image;
-            AZ::Data::Instance<AZ::RPI::Image> m_occlusionImage;
-            AZ::Data::Instance<AZ::RPI::Image> m_heightImage;
-        };
-
-        struct DetailMaterialSurface
-        {
-            AZ::Crc32 m_surfaceTag;
-            uint16_t m_detailMaterialId;
-        };
-
-        struct DetailMaterialListRegion
-        {
-            AZ::EntityId m_entityId;
-            AZ::Aabb m_region{AZ::Aabb::CreateNull()};
-            AZStd::vector<DetailMaterialSurface> m_materialsForSurfaces;
-        };
-        
-        struct Vector2i
-        {
-            int32_t m_x{ 0 };
-            int32_t m_y{ 0 };
-
-            Vector2i operator+(const Vector2i& rhs) const;
-            Vector2i& operator+=(const Vector2i& rhs);
-            Vector2i operator-(const Vector2i& rhs) const;
-            Vector2i& operator-=(const Vector2i& rhs);
-            Vector2i operator-() const;
-        };
-
-        struct Aabb2i
-        {
-            Vector2i m_min;
-            Vector2i m_max;
-
-            Aabb2i operator+(const Vector2i& offset) const;
-            Aabb2i operator-(const Vector2i& offset) const;
-
-            Aabb2i GetClamped(Aabb2i rhs) const;
-            bool IsValid() const;
-        };
+        static constexpr auto InvalidImageIndex = AZ::Render::BindlessImageArrayHandler::InvalidImageIndex;
+        using MaterialInstance = AZ::Data::Instance<AZ::RPI::Material>;
         
-        struct DetailTextureLocation
+        struct WorldShaderData
         {
-            uint16_t m_index;
-            AZ::Data::Instance<AZ::RPI::Image> m_image;
+            AZStd::array<float, 3> m_min{ 0.0f, 0.0f, 0.0f };
+            float padding1{ 0.0f };
+            AZStd::array<float, 3> m_max{ 0.0f, 0.0f, 0.0f };
+            float padding2{ 0.0f };
         };
 
         // AZ::RPI::MaterialReloadNotificationBus::Handler overrides...
@@ -254,122 +74,46 @@ namespace Terrain
         void OnTerrainDataDestroyBegin() override;
         void OnTerrainDataChanged(const AZ::Aabb& dirtyRegion, TerrainDataChangedMask dataChangedMask) override;
 
-        // TerrainMacroMaterialNotificationBus overrides...
-        void OnTerrainMacroMaterialCreated(AZ::EntityId entityId, const MacroMaterialData& material) override;
-        void OnTerrainMacroMaterialChanged(AZ::EntityId entityId, const MacroMaterialData& material) override;
-        void OnTerrainMacroMaterialRegionChanged(AZ::EntityId entityId, const AZ::Aabb& oldRegion, const AZ::Aabb& newRegion) override;
-        void OnTerrainMacroMaterialDestroyed(AZ::EntityId entityId) override;
-        
-        // TerrainAreaMaterialNotificationBus overrides...
-        void OnTerrainSurfaceMaterialMappingCreated(AZ::EntityId entityId, SurfaceData::SurfaceTag surfaceTag, MaterialInstance material) override;
-        void OnTerrainSurfaceMaterialMappingDestroyed(AZ::EntityId entityId, SurfaceData::SurfaceTag surfaceTag) override;
-        void OnTerrainSurfaceMaterialMappingChanged(AZ::EntityId entityId, SurfaceData::SurfaceTag surfaceTag, MaterialInstance material) override;
-        void OnTerrainSurfaceMaterialMappingRegionChanged(AZ::EntityId entityId, const AZ::Aabb& oldRegion, const AZ::Aabb& newRegion) override;
-
         // AZ::RPI::SceneNotificationBus overrides...
         void OnRenderPipelinePassesChanged(AZ::RPI::RenderPipeline* renderPipeline) override;
 
         void Initialize();
-        void InitializeTerrainPatch(uint16_t gridSize, float gridSpacing, PatchData& patchdata);
-        bool InitializePatchModel();
 
-        void UpdateTerrainData();
+        void UpdateHeightmapImage();
         void PrepareMaterialData();
-        void UpdateMacroMaterialData(MacroMaterialData& macroMaterialData, const MacroMaterialData& newMaterialData);
-        
-        void TerrainHeightOrSettingsUpdated(const AZ::Aabb& dirtyRegion);
-        void TerrainSurfaceDataUpdated(const AZ::Aabb& dirtyRegion);
 
-        uint16_t CreateOrUpdateDetailMaterial(MaterialInstance material);
-        void CheckDetailMaterialForDeletion(uint16_t detailMaterialId);
-        void UpdateDetailMaterialData(uint16_t detailMaterialIndex, MaterialInstance material);
-        void CheckUpdateDetailTexture(const Aabb2i& newBounds, const Vector2i& newCenter);
-        void UpdateDetailTexture(const Aabb2i& updateArea, const Aabb2i& textureBounds, const Vector2i& centerPixel);
-        uint16_t GetDetailMaterialForSurfaceTypeAndPosition(AZ::Crc32 surfaceType, const AZ::Vector2& position);
-        uint8_t CalculateUpdateRegions(const Aabb2i& updateArea, const Aabb2i& textureBounds, const Vector2i& centerPixel,
-            AZStd::array<Aabb2i, 4>& textureSpaceAreas, AZStd::array<Aabb2i, 4>& scaledWorldSpaceAreas);
+        void TerrainHeightOrSettingsUpdated(const AZ::Aabb& dirtyRegion);
 
         void ProcessSurfaces(const FeatureProcessor::RenderPacket& process);
 
-        template <typename T>
-        T* FindByEntityId(AZ::EntityId entityId, AZ::Render::IndexedDataVector<T>& container);
-        template <typename T>
-        T& FindOrCreateByEntityId(AZ::EntityId entityId, AZ::Render::IndexedDataVector<T>& container);
-        template <typename T>
-        void RemoveByEntityId(AZ::EntityId entityId, AZ::Render::IndexedDataVector<T>& container);
-
-        template<typename Callback>
-        void ForOverlappingSectors(const AZ::Aabb& bounds, Callback callback);
-
-        AZ::Outcome<AZ::Data::Asset<AZ::RPI::BufferAsset>> CreateBufferAsset(
-            const void* data, const AZ::RHI::BufferViewDescriptor& bufferViewDescriptor, const AZStd::string& bufferName);
-
         void CacheForwardPass();
 
-        // System-level parameters
-        static constexpr float GridSpacing{ 1.0f };
-        static constexpr int32_t GridSize{ 64 }; // number of terrain quads (vertices are m_gridSize + 1)
-        static constexpr float GridMeters{ GridSpacing * GridSize };
-        static constexpr int32_t DetailTextureSize{ 1024 };
-        static constexpr int32_t DetailTextureSizeHalf{ DetailTextureSize / 2 };
-        static constexpr float DetailTextureScale{ 0.5f };
+        TerrainMeshManager m_meshManager;
+        TerrainMacroMaterialManager m_macroMaterialManager;
+        TerrainDetailMaterialManager m_detailMaterialManager;
+
+        AZStd::shared_ptr<AZ::Render::BindlessImageArrayHandler> m_imageArrayHandler;
 
         AZStd::unique_ptr<AZ::RPI::AssetUtils::AsyncAssetLoader> m_materialAssetLoader;
         MaterialInstance m_materialInstance;
+
         AZ::Data::Instance<AZ::RPI::ShaderResourceGroup> m_terrainSrg;
+        AZ::Data::Instance<AZ::RPI::AttachmentImage> m_heightmapImage;
 
-        AZ::RHI::ShaderInputConstantIndex m_modelToWorldIndex;
-        AZ::RHI::ShaderInputConstantIndex m_terrainDataIndex;
-        AZ::RHI::ShaderInputConstantIndex m_macroMaterialDataIndex;
-        AZ::RHI::ShaderInputConstantIndex m_macroMaterialCountIndex;
-        AZ::RHI::ShaderInputImageIndex m_macroColorMapIndex;
-        AZ::RHI::ShaderInputImageIndex m_macroNormalMapIndex;
         AZ::RHI::ShaderInputImageIndex m_heightmapPropertyIndex;
-        AZ::RHI::ShaderInputImageIndex m_detailMaterialIdPropertyIndex;
-        AZ::RHI::ShaderInputBufferIndex m_detailMaterialDataIndex;
-        AZ::RHI::ShaderInputConstantIndex m_detailCenterPropertyIndex;
-        AZ::RHI::ShaderInputConstantIndex m_detailAabbPropertyIndex;
-        AZ::RHI::ShaderInputConstantIndex m_detailHalfPixelUvPropertyIndex;
-        AZ::RHI::ShaderInputImageUnboundedArrayIndex m_detailTexturesIndex;
+        AZ::RHI::ShaderInputConstantIndex m_worldDataIndex;
 
-        AZ::Data::Instance<AZ::RPI::Model> m_patchModel;
-        AZ::Vector3 m_previousCameraPosition = AZ::Vector3(AZStd::numeric_limits<float>::max(), 0.0, 0.0);
-
-        // Per-area data
-        struct TerrainAreaData
-        {
-            AZ::Transform m_transform{ AZ::Transform::CreateIdentity() };
-            AZ::Aabb m_terrainBounds{ AZ::Aabb::CreateNull() };
-            AZ::Data::Instance<AZ::RPI::AttachmentImage> m_heightmapImage;
-            float m_sampleSpacing{ 0.0f };
-            bool m_heightmapUpdated{ true };
-            bool m_macroMaterialsUpdated{ true };
-            bool m_rebuildSectors{ true };
-        };
-        
-        TerrainAreaData m_areaData;
+        AZ::Aabb m_terrainBounds{ AZ::Aabb::CreateNull() };
         AZ::Aabb m_dirtyRegion{ AZ::Aabb::CreateNull() };
-        AZ::Aabb m_dirtyDetailRegion{ AZ::Aabb::CreateNull() };
-        bool m_updateDetailMaterialBuffer{ false };
-
-        Aabb2i m_detailTextureBounds;
-        Vector2i m_detailTextureCenter;
-        AZ::Data::Instance<AZ::RPI::AttachmentImage> m_detailTextureImage;
-        AZ::RPI::ShaderSystemInterface::GlobalShaderOptionUpdatedEvent::Handler m_handleGlobalShaderOptionUpdate;
+        
+        float m_sampleSpacing{ 0.0f };
+        
+        bool m_heightmapNeedsUpdate{ false };
         bool m_forceRebuildDrawPackets{ false };
-        bool m_imagesNeedUpdate{ false };
+        bool m_imageBindingsNeedUpdate{ false };
 
-        AZStd::vector<SectorData> m_sectorData;
+        AZ::RPI::ShaderSystemInterface::GlobalShaderOptionUpdatedEvent::Handler m_handleGlobalShaderOptionUpdate;
 
-        AZ::Render::IndexedDataVector<MacroMaterialData> m_macroMaterials;
-        AZ::Render::IndexedDataVector<DetailMaterialData> m_detailMaterials;
-        AZ::Render::IndexedDataVector<DetailMaterialListRegion> m_detailMaterialRegions;
-        AZ::Render::SparseVector<DetailMaterialShaderData> m_detailMaterialShaderData;
-        AZ::Render::GpuBufferHandler m_detailMaterialDataBuffer;
         AZ::RPI::RenderPass* m_forwardPass;
-
-        AZStd::vector<const AZ::RHI::ImageView*> m_detailImageViews;
-        AZStd::vector<uint16_t> m_detailImageViewFreeList;
-        bool m_detailImagesNeedUpdate{ false };
     };
 }

+ 19 - 0
Gems/Terrain/Code/Source/TerrainRenderer/TerrainMacroMaterialBus.cpp

@@ -66,8 +66,27 @@ namespace Terrain
         }
     };
 
+    void MacroMaterialData::Reflect(AZ::ReflectContext* context)
+    {
+        if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
+        {
+            behaviorContext->Class<MacroMaterialData>()
+                ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
+                ->Attribute(AZ::Script::Attributes::Category, "Terrain")
+                ->Attribute(AZ::Script::Attributes::Module, "terrain")
+                ->Property("EntityId", BehaviorValueProperty(&MacroMaterialData::m_entityId))
+                ->Property("Bounds", BehaviorValueProperty(&MacroMaterialData::m_bounds))
+                ->Property("NormalFlipX", BehaviorValueProperty(&MacroMaterialData::m_normalFlipX))
+                ->Property("NormalFlipY", BehaviorValueProperty(&MacroMaterialData::m_normalFlipY))
+                ->Property("NormalFactor", BehaviorValueProperty(&MacroMaterialData::m_normalFactor))
+                ;
+        }
+    }
+
     void TerrainMacroMaterialRequests::Reflect(AZ::ReflectContext* context)
     {
+        MacroMaterialData::Reflect(context);
+
         if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
         {
             behaviorContext->EBus<Terrain::TerrainMacroMaterialRequestBus>("TerrainMacroMaterialRequestBus")

+ 4 - 2
Gems/Terrain/Code/Source/TerrainRenderer/TerrainMacroMaterialBus.h

@@ -18,8 +18,10 @@ namespace Terrain
 {
     struct MacroMaterialData final
     {
-        AZ_RTTI(MacroMaterialData, "{DC68E20A-3251-4E4E-8BC7-F6A2521FEF46}");
-                 
+        AZ_TYPE_INFO(MacroMaterialData, "{DC68E20A-3251-4E4E-8BC7-F6A2521FEF46}");
+
+        static void Reflect(AZ::ReflectContext* context);
+
         AZ::EntityId m_entityId;
         AZ::Aabb m_bounds = AZ::Aabb::CreateNull();
 

+ 412 - 0
Gems/Terrain/Code/Source/TerrainRenderer/TerrainMacroMaterialManager.cpp

@@ -0,0 +1,412 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <TerrainRenderer/TerrainMacroMaterialManager.h>
+
+namespace Terrain
+{
+    namespace
+    {
+        [[maybe_unused]] static const char* TerrainMacroMaterialManagerName = "TerrainMacroMaterialManager";
+    }
+
+    namespace TerrainSrgInputs
+    {
+        static const char* const MacroMaterialData("m_macroMaterialData");
+        static const char* const MacroMaterialGrid("m_macroMaterialGrid");
+    }
+    
+    void TerrainMacroMaterialManager::Initialize(
+        const AZStd::shared_ptr<AZ::Render::BindlessImageArrayHandler>& bindlessImageHandler,
+        AZ::Data::Instance<AZ::RPI::ShaderResourceGroup>& terrainSrg)
+    {
+        AZ_Error(TerrainMacroMaterialManagerName, bindlessImageHandler, "bindlessImageHandler must not be null.");
+        AZ_Error(TerrainMacroMaterialManagerName, terrainSrg, "terrainSrg must not be null.");
+        AZ_Error(TerrainMacroMaterialManagerName, !m_isInitialized, "Already initialized.");
+
+        if (!bindlessImageHandler || !terrainSrg || m_isInitialized)
+        {
+            return;
+        }
+        
+        if (UpdateSrgIndices(terrainSrg))
+        {
+            m_bindlessImageHandler = bindlessImageHandler;
+
+            OnTerrainDataChanged(AZ::Aabb::CreateNull(), TerrainDataChangedMask::Settings);
+            AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusConnect();
+            TerrainMacroMaterialNotificationBus::Handler::BusConnect();
+            
+            m_terrainSizeChanged = true;
+            m_isInitialized = true;
+        }
+    }
+    
+    void TerrainMacroMaterialManager::Reset()
+    {
+        m_isInitialized = false;
+
+        m_macroMaterialDataBuffer = {};
+
+        m_macroMaterialShaderData.clear();
+        m_macroMaterialEntities.clear();
+
+        RemoveAllImages();
+        m_macroMaterials.clear();
+        
+        m_bindlessImageHandler = {};
+
+        AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusDisconnect();
+        TerrainMacroMaterialNotificationBus::Handler::BusDisconnect();
+    }
+    
+    bool TerrainMacroMaterialManager::IsInitialized()
+    {
+        return m_isInitialized;
+    }
+    
+    bool TerrainMacroMaterialManager::UpdateSrgIndices(AZ::Data::Instance<AZ::RPI::ShaderResourceGroup>& terrainSrg)
+    {
+        const AZ::RHI::ShaderResourceGroupLayout* terrainSrgLayout = terrainSrg->GetLayout();
+            
+        m_macroMaterialGridIndex = terrainSrgLayout->FindShaderInputConstantIndex(AZ::Name(TerrainSrgInputs::MacroMaterialGrid));
+        AZ_Error(TerrainMacroMaterialManagerName, m_macroMaterialGridIndex.IsValid(), "Failed to find terrain srg input constant %s.", TerrainSrgInputs::MacroMaterialGrid);
+        
+        AZ::Render::GpuBufferHandler::Descriptor desc;
+
+        // Set up the gpu buffer for macro material data
+        desc.m_bufferName = "Macro Material Data";
+        desc.m_bufferSrgName = TerrainSrgInputs::MacroMaterialData;
+        desc.m_elementSize = sizeof(MacroMaterialShaderData);
+        desc.m_srgLayout = terrainSrgLayout;
+        m_macroMaterialDataBuffer = AZ::Render::GpuBufferHandler(desc);
+
+        m_bufferNeedsUpdate = true;
+
+        return m_macroMaterialDataBuffer.IsValid() && m_macroMaterialGridIndex.IsValid();
+    }
+
+    void TerrainMacroMaterialManager::OnTerrainDataChanged(const AZ::Aabb& dirtyRegion [[maybe_unused]], TerrainDataChangedMask dataChangedMask)
+    {
+        if ((dataChangedMask & TerrainDataChangedMask::Settings) != 0)
+        {
+            AZ::Aabb worldBounds = AZ::Aabb::CreateNull();
+            AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
+                worldBounds, &AzFramework::Terrain::TerrainDataRequests::GetTerrainAabb);
+
+            m_terrainSizeChanged = m_terrainSizeChanged || m_terrainBounds != worldBounds;
+            m_terrainBounds = worldBounds;
+        }
+    }
+    
+    void TerrainMacroMaterialManager::OnTerrainMacroMaterialCreated(AZ::EntityId entityId, const MacroMaterialData& newMaterialData)
+    {
+        AZ_Assert(!m_macroMaterials.contains(entityId),
+            "OnTerrainMacroMaterialCreated called for a macro material that already exists. This indicates that either the bus is incorrectly sending out "
+            "OnCreated announcements for existing materials, or the terrain feature processor isn't properly cleaning up macro materials.");
+
+        MacroMaterial& macroMaterial = m_macroMaterials[entityId];
+        macroMaterial.m_data = newMaterialData;
+        if (newMaterialData.m_colorImage)
+        {
+            macroMaterial.m_colorIndex = m_bindlessImageHandler->AppendBindlessImage(newMaterialData.m_colorImage->GetImageView());
+        }
+        if (newMaterialData.m_normalImage)
+        {
+            macroMaterial.m_normalIndex = m_bindlessImageHandler->AppendBindlessImage(newMaterialData.m_normalImage->GetImageView());
+        }
+
+        ForMacroMaterialsInBounds(newMaterialData.m_bounds,
+            [&](uint16_t idx, [[maybe_unused]] const AZ::Vector2& corner)
+            {
+                for (uint16_t offset = 0; offset < MacroMaterialsPerTile; ++offset)
+                {
+                    MacroMaterialShaderData& macroMaterialShaderData = m_macroMaterialShaderData.at(idx + offset);
+                    if ((macroMaterialShaderData.m_flags & MacroMaterialShaderFlags::IsUsed) == 0)
+                    {
+                        UpdateMacroMaterialShaderEntry(idx + offset, macroMaterial);
+                        break;
+                    }
+                    AZ_Assert(m_macroMaterialEntities.at(idx + offset) != entityId, "Found existing macro material tile for what should be a completely new macro material.");
+                }
+            }
+        );
+
+        m_bufferNeedsUpdate = true;
+    }
+
+    void TerrainMacroMaterialManager::OnTerrainMacroMaterialChanged(AZ::EntityId entityId, const MacroMaterialData& newMaterialData)
+    {
+        AZ_Assert(m_macroMaterials.contains(entityId),
+            "OnTerrainMacroMaterialChanged called for a macro material that TerrainFeatureProcessor isn't tracking. This indicates that either the bus is sending out "
+            "Changed announcements for materials that haven't had a OnCreated event sent, or the terrain feature processor isn't properly tracking macro materials.");
+        
+        MacroMaterial& macroMaterial = m_macroMaterials[entityId];
+        macroMaterial.m_data = newMaterialData;
+
+        auto UpdateImageIndex = [&](uint16_t& indexRef, const AZ::Data::Instance<AZ::RPI::Image>& imageView)
+        {
+            if (indexRef)
+            {
+                if (imageView)
+                {
+                    m_bindlessImageHandler->UpdateBindlessImage(indexRef, imageView->GetImageView());
+                }
+                else
+                {
+                    m_bindlessImageHandler->RemoveBindlessImage(indexRef);
+                    indexRef = 0xFFFF;
+                }
+            }
+            else if (imageView)
+            {
+                indexRef = m_bindlessImageHandler->AppendBindlessImage(imageView->GetImageView());
+            }
+        };
+
+        UpdateImageIndex(macroMaterial.m_colorIndex, newMaterialData.m_colorImage);
+        UpdateImageIndex(macroMaterial.m_normalIndex, newMaterialData.m_normalImage);
+        
+        ForMacroMaterialsInBounds(newMaterialData.m_bounds,
+            [&](uint16_t idx, [[maybe_unused]] const AZ::Vector2& corner)
+            {
+                for (uint16_t offset = 0; offset < MacroMaterialsPerTile; ++offset)
+                {
+                    if (m_macroMaterialEntities.at(idx + offset) == entityId)
+                    {
+                        UpdateMacroMaterialShaderEntry(idx + offset, macroMaterial);
+                        break;
+                    }
+                }
+            }
+        );
+        
+        m_bufferNeedsUpdate = true;
+    }
+    
+    void TerrainMacroMaterialManager::OnTerrainMacroMaterialRegionChanged(
+        AZ::EntityId entityId, [[maybe_unused]] const AZ::Aabb& oldRegion, const AZ::Aabb& newRegion)
+    {
+        AZ_Assert(m_macroMaterials.contains(entityId),
+            "OnTerrainMacroMaterialChanged called for a macro material that TerrainFeatureProcessor isn't tracking. This indicates that either the bus is sending out "
+            "Changed announcements for materials that haven't had a OnCreated event sent, or the terrain feature processor isn't properly tracking macro materials.");
+        
+        MacroMaterial& macroMaterial = m_macroMaterials[entityId];
+        macroMaterial.m_data.m_bounds = newRegion;
+
+        AZ::Aabb changedRegion = oldRegion;
+        changedRegion.AddAabb(newRegion);
+
+        ForMacroMaterialsInBounds(changedRegion,
+            [&](uint16_t idx, const AZ::Vector2& corner)
+            {
+                AZ::Aabb tileAabb = AZ::Aabb::CreateFromMinMaxValues(
+                    corner.GetX(), corner.GetY(), m_terrainBounds.GetMin().GetZ(),
+                    corner.GetX() + MacroMaterialGridSize, corner.GetY() + MacroMaterialGridSize, m_terrainBounds.GetMax().GetZ());
+
+                bool overlapsNew = tileAabb.Overlaps(newRegion);
+                uint16_t end = idx + MacroMaterialsPerTile;
+
+                for (; idx < end; ++idx)
+                {
+                    if (m_macroMaterialEntities.at(idx) == entityId)
+                    {
+                        if (overlapsNew)
+                        {
+                            // Update the macro material entry from this tile.
+                            UpdateMacroMaterialShaderEntry(idx, macroMaterial);
+                        }
+                        else
+                        {
+                            // Remove the macro material entry from this tile.
+                            RemoveMacroMaterialShaderEntry(idx);
+                        }
+                        break;
+                    }
+                    else if (overlapsNew && (m_macroMaterialShaderData.at(idx).m_flags & MacroMaterialShaderFlags::IsUsed) == 0)
+                    {
+                        // Add a macro material entry from this tile. (!overlapsOld && overlapsNew)
+                        UpdateMacroMaterialShaderEntry(idx, macroMaterial);
+                        break;
+                    }
+                }
+            }
+        );
+        
+        m_bufferNeedsUpdate = true;
+    }
+
+    void TerrainMacroMaterialManager::OnTerrainMacroMaterialDestroyed(AZ::EntityId entityId)
+    {
+        AZ_Assert(m_macroMaterials.contains(entityId),
+            "OnTerrainMacroMaterialChanged called for a macro material that TerrainFeatureProcessor isn't tracking. This indicates that either the bus is sending out "
+            "Changed announcements for materials that haven't had a OnCreated event sent, or the terrain feature processor isn't properly tracking macro materials.");
+        
+        const MacroMaterial& macroMaterial = m_macroMaterials[entityId];
+        
+        ForMacroMaterialsInBounds(macroMaterial.m_data.m_bounds,
+            [&](uint16_t idx, [[maybe_unused]] const AZ::Vector2& corner)
+            {
+                uint16_t end = idx + MacroMaterialsPerTile;
+
+                for (; idx < end; ++idx)
+                {
+                    if (m_macroMaterialEntities.at(idx) == entityId)
+                    {
+                        RemoveMacroMaterialShaderEntry(idx);
+                    }
+                }
+            }
+        );
+        
+        if (macroMaterial.m_colorIndex != 0xFFFF)
+        {
+            m_bindlessImageHandler->RemoveBindlessImage(macroMaterial.m_colorIndex);
+        }
+        if (macroMaterial.m_normalIndex != 0xFFFF)
+        {
+            m_bindlessImageHandler->RemoveBindlessImage(macroMaterial.m_normalIndex);
+        }
+
+        m_macroMaterials.erase(entityId);
+        m_bufferNeedsUpdate = true;
+    }
+    
+    void TerrainMacroMaterialManager::UpdateMacroMaterialShaderEntry(uint16_t shaderDataIdx, const MacroMaterial& macroMaterial)
+    {
+        m_macroMaterialEntities.at(shaderDataIdx) = macroMaterial.m_data.m_entityId;
+        MacroMaterialShaderData& macroMaterialShaderData = m_macroMaterialShaderData.at(shaderDataIdx);
+
+        macroMaterialShaderData.m_flags = (MacroMaterialShaderFlags)(
+            MacroMaterialShaderFlags::IsUsed |
+            (macroMaterial.m_data.m_normalFlipX ? MacroMaterialShaderFlags::FlipMacroNormalX : 0) |
+            (macroMaterial.m_data.m_normalFlipY ? MacroMaterialShaderFlags::FlipMacroNormalY : 0)
+        );
+
+        macroMaterialShaderData.m_normalFactor = macroMaterial.m_data.m_normalFactor;
+        macroMaterialShaderData.m_boundsMin = { macroMaterial.m_data.m_bounds.GetMin().GetX(), macroMaterial.m_data.m_bounds.GetMin().GetY() };
+        macroMaterialShaderData.m_boundsMax = { macroMaterial.m_data.m_bounds.GetMax().GetX(), macroMaterial.m_data.m_bounds.GetMax().GetY() };
+        macroMaterialShaderData.m_colorMapId = macroMaterial.m_colorIndex;
+        macroMaterialShaderData.m_normalMapId = macroMaterial.m_normalIndex;
+    }
+
+    void TerrainMacroMaterialManager::RemoveMacroMaterialShaderEntry(uint16_t shaderDataIdx)
+    {
+        // Remove the macro material entry from this tile by copying the remaining entries on top.
+        for (++shaderDataIdx; shaderDataIdx % MacroMaterialsPerTile != 0; ++shaderDataIdx)
+        {
+            m_macroMaterialEntities.at(shaderDataIdx - 1) = m_macroMaterialEntities.at(shaderDataIdx);
+            m_macroMaterialShaderData.at(shaderDataIdx - 1) = m_macroMaterialShaderData.at(shaderDataIdx);
+        }
+        // Disable the last entry.
+        m_macroMaterialEntities.at(shaderDataIdx - 1) = AZ::EntityId();
+        m_macroMaterialShaderData.at(shaderDataIdx - 1).m_flags = MacroMaterialShaderFlags(0);
+    }
+
+    template<typename Callback>
+    void TerrainMacroMaterialManager::ForMacroMaterialsInBounds(const AZ::Aabb& bounds, Callback callback)
+    {
+        // Get the macro material bounds relative to the terrain
+        float yStart = bounds.GetMin().GetY() - m_terrainBounds.GetMin().GetY();
+        float yEnd = bounds.GetMax().GetY() - m_terrainBounds.GetMin().GetY();
+        float xStart = bounds.GetMin().GetX() - m_terrainBounds.GetMin().GetX();
+        float xEnd = bounds.GetMax().GetX() - m_terrainBounds.GetMin().GetX();
+
+        // Clamp the bounds to the terrain
+        uint16_t yStartIdx = yStart > 0.0f ? uint16_t(yStart / MacroMaterialGridSize) : 0;
+        uint16_t yEndIdx = yEnd > 0.0f ? AZStd::GetMin<uint16_t>(uint16_t(yEnd / MacroMaterialGridSize) + 1, m_tilesY) : 0;
+        uint16_t xStartIdx = xStart > 0.0f ? uint16_t(xStart / MacroMaterialGridSize) : 0;
+        uint16_t xEndIdx = xEnd > 0.0f ? AZStd::GetMin<uint16_t>(uint16_t(xEnd / MacroMaterialGridSize) + 1, m_tilesX) : 0;
+
+        AZ::Vector2 gridCorner = AZ::Vector2(
+            floor(m_terrainBounds.GetMin().GetX() / MacroMaterialGridSize) * MacroMaterialGridSize,
+            floor(m_terrainBounds.GetMin().GetY() / MacroMaterialGridSize) * MacroMaterialGridSize);
+
+        for (uint16_t y = yStartIdx; y < yEndIdx; ++y)
+        {
+            for (uint16_t x = xStartIdx; x < xEndIdx; ++x)
+            {
+                uint16_t idx = (y * m_tilesX + x) * MacroMaterialsPerTile;
+                const AZ::Vector2 corner = gridCorner + AZ::Vector2(x * MacroMaterialGridSize, y * MacroMaterialGridSize);
+                callback(idx, corner);
+            }
+        }
+    }
+
+    void TerrainMacroMaterialManager::Update(AZ::Data::Instance<AZ::RPI::ShaderResourceGroup>& terrainSrg)
+    {
+        if (m_terrainSizeChanged)
+        {
+            m_terrainSizeChanged = false;
+
+            // Rebuild the macro material tiles from scratch when the world size changes. This could be made more efficient
+            // but is fine for now since world resizes are rare.
+
+            RemoveAllImages();
+            m_macroMaterials.clear();
+
+            m_macroMaterialShaderData.clear();
+            m_macroMaterialEntities.clear();
+            
+            m_tilesX = aznumeric_cast<uint16_t>(m_terrainBounds.GetXExtent() / MacroMaterialGridSize) + 1;
+            m_tilesY = aznumeric_cast<uint16_t>(m_terrainBounds.GetYExtent() / MacroMaterialGridSize) + 1;
+            const uint32_t macroMaterialTileCount = m_tilesX * m_tilesY * MacroMaterialsPerTile;
+            
+            m_macroMaterialShaderData.resize(macroMaterialTileCount);
+            m_macroMaterialEntities.resize(macroMaterialTileCount);
+
+            TerrainMacroMaterialRequestBus::EnumerateHandlers(
+                [&](TerrainMacroMaterialRequests* handler)
+                {
+                    MacroMaterialData macroMaterial = handler->GetTerrainMacroMaterialData();
+                    AZ::EntityId entityId = *(Terrain::TerrainMacroMaterialRequestBus::GetCurrentBusId());
+                    OnTerrainMacroMaterialCreated(entityId, macroMaterial);
+                    return true;
+                }
+            );
+        }
+
+        if (m_bufferNeedsUpdate)
+        {
+            m_bufferNeedsUpdate = false;
+            m_macroMaterialDataBuffer.UpdateBuffer(m_macroMaterialShaderData.data(), aznumeric_cast<uint32_t>(m_macroMaterialShaderData.size()));
+
+            MacroMaterialGridShaderData macroMaterialGridShaderData;
+            macroMaterialGridShaderData.m_offset = { m_terrainBounds.GetMin().GetX(), m_terrainBounds.GetMin().GetY() };
+            macroMaterialGridShaderData.m_resolution = (m_tilesX << 16) | m_tilesY;
+            macroMaterialGridShaderData.m_tileSize = MacroMaterialGridSize;
+
+            if (terrainSrg)
+            {   
+                m_macroMaterialDataBuffer.UpdateSrg(terrainSrg.get());
+                terrainSrg->SetConstant(m_macroMaterialGridIndex, macroMaterialGridShaderData);
+            }
+        }
+    }
+
+    void TerrainMacroMaterialManager::RemoveAllImages()
+    {   
+        for (const auto& [entity, macroMaterial] : m_macroMaterials)
+        {
+            RemoveImagesForMaterial(macroMaterial);
+        }
+    }
+
+    void TerrainMacroMaterialManager::RemoveImagesForMaterial(const MacroMaterial& macroMaterial)
+    {
+        if (macroMaterial.m_colorIndex != 0xFFFF)
+        {
+            m_bindlessImageHandler->RemoveBindlessImage(macroMaterial.m_colorIndex);
+        }
+        if (macroMaterial.m_normalIndex != 0xFFFF)
+        {
+            m_bindlessImageHandler->RemoveBindlessImage(macroMaterial.m_normalIndex);
+        }
+    }
+
+}

+ 116 - 0
Gems/Terrain/Code/Source/TerrainRenderer/TerrainMacroMaterialManager.h

@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <AzFramework/Terrain/TerrainDataRequestBus.h>
+#include <Atom/RPI.Public/Shader/ShaderResourceGroup.h>
+#include <Atom/Feature/Utils/GpuBufferHandler.h>
+#include <TerrainRenderer/BindlessImageArrayHandler.h>
+#include <TerrainRenderer/TerrainMacroMaterialBus.h>
+
+namespace Terrain
+{
+    class TerrainMacroMaterialManager
+        : private TerrainMacroMaterialNotificationBus::Handler
+        , private AzFramework::Terrain::TerrainDataNotificationBus::Handler
+    {
+    public:
+        
+        TerrainMacroMaterialManager() = default;
+        ~TerrainMacroMaterialManager() = default;
+
+        void Initialize(
+            const AZStd::shared_ptr<AZ::Render::BindlessImageArrayHandler>& bindlessImageHandler,
+            AZ::Data::Instance<AZ::RPI::ShaderResourceGroup>& terrainSrg);
+        void Reset();
+        bool IsInitialized();
+        bool UpdateSrgIndices(AZ::Data::Instance<AZ::RPI::ShaderResourceGroup>& terrainSrg);
+        
+        void Update(AZ::Data::Instance<AZ::RPI::ShaderResourceGroup>& terrainSrg);
+
+    private:
+        
+        static constexpr auto InvalidImageIndex = AZ::Render::BindlessImageArrayHandler::InvalidImageIndex;
+        static constexpr float MacroMaterialGridSize = 64.0f;
+        static constexpr uint16_t MacroMaterialsPerTile = 4;
+
+        enum MacroMaterialShaderFlags : uint32_t
+        {
+            IsUsed           = 0b0000'0000'0000'0000'0000'0000'0000'0001,
+            FlipMacroNormalX = 0b0000'0000'0000'0000'0000'0000'0000'0010,
+            FlipMacroNormalY = 0b0000'0000'0000'0000'0000'0000'0000'0100,
+        };
+
+        struct MacroMaterialShaderData
+        {
+            MacroMaterialShaderFlags m_flags;
+            uint32_t m_colorMapId{InvalidImageIndex};
+            uint32_t m_normalMapId{InvalidImageIndex};
+            float m_normalFactor;
+
+            // macro material bounds in world space
+            AZStd::array<float, 2> m_boundsMin{ 0.0f, 0.0f };
+            AZStd::array<float, 2> m_boundsMax{ 0.0f, 0.0f };
+        };
+        static_assert(sizeof(MacroMaterialShaderData) % 16 == 0, "MacroMaterialShaderData must be 16 byte aligned.");
+        
+        struct MacroMaterial
+        {
+            MacroMaterialData m_data;
+            uint16_t m_colorIndex{ 0xFFFF };
+            uint16_t m_normalIndex{ 0xFFFF };
+        };
+
+        struct MacroMaterialGridShaderData
+        {
+            uint32_t m_resolution; // How many x/y tiles in grid. x & y stored in 16 bits each. Total number of entries in m_macroMaterialData will be x * y
+            float m_tileSize; // Size of a tile in meters.
+            AZStd::array<float, 2> m_offset; // x/y offset of min x/y corner of grid.
+        };
+        static_assert(sizeof(MacroMaterialGridShaderData) % 16 == 0, "MacroMaterialGridShaderData must be 16 byte aligned.");
+        
+        // AzFramework::Terrain::TerrainDataNotificationBus overrides...
+        void OnTerrainDataChanged(const AZ::Aabb& dirtyRegion [[maybe_unused]], TerrainDataChangedMask dataChangedMask) override;
+
+        // TerrainMacroMaterialNotificationBus overrides...
+        void OnTerrainMacroMaterialCreated(AZ::EntityId entityId, const MacroMaterialData& material) override;
+        void OnTerrainMacroMaterialChanged(AZ::EntityId entityId, const MacroMaterialData& material) override;
+        void OnTerrainMacroMaterialRegionChanged(AZ::EntityId entityId, const AZ::Aabb& oldRegion, const AZ::Aabb& newRegion) override;
+        void OnTerrainMacroMaterialDestroyed(AZ::EntityId entityId) override;
+        
+        void UpdateMacroMaterialShaderEntry(uint16_t shaderDataIdx, const MacroMaterial& macroMaterialData);
+        void RemoveMacroMaterialShaderEntry(uint16_t shaderDataIdx);
+
+        template<typename Callback>
+        void ForMacroMaterialsInBounds(const AZ::Aabb& bounds, Callback callback);
+
+        void RemoveAllImages();
+        void RemoveImagesForMaterial(const MacroMaterial& macroMaterial);
+
+        AZ::Aabb m_terrainBounds{ AZ::Aabb::CreateNull() };
+
+        // Macro materials stored in a grid of (MacroMaterialGridCount * MacroMaterialGridCount) where each tile in the grid covers
+        // an area of (MacroMaterialGridSize * MacroMaterialGridSize) and each tile can hold MacroMaterialsPerTile macro materials
+        AZStd::vector<MacroMaterialShaderData> m_macroMaterialShaderData;
+        AZStd::vector<AZ::EntityId> m_macroMaterialEntities; // Same as above, but used to track entity ids which aren't needed by the shader.
+        AZStd::map<AZ::EntityId, MacroMaterial> m_macroMaterials; // Used for looking up macro materials by entity id when the data isn't provided by a bus.
+        uint16_t m_tilesX{ 0 };
+        uint16_t m_tilesY{ 0 };
+
+        AZStd::shared_ptr<AZ::Render::BindlessImageArrayHandler> m_bindlessImageHandler;
+        AZ::Render::GpuBufferHandler m_macroMaterialDataBuffer;
+
+        AZ::RHI::ShaderInputConstantIndex m_macroMaterialGridIndex;
+
+        bool m_terrainSizeChanged{ false };
+        bool m_bufferNeedsUpdate{ false };
+        bool m_isInitialized{ false };
+
+    };
+}

+ 354 - 0
Gems/Terrain/Code/Source/TerrainRenderer/TerrainMeshManager.cpp

@@ -0,0 +1,354 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <TerrainRenderer/TerrainMeshManager.h>
+
+#include <AzCore/Math/Frustum.h>
+
+#include <Atom/RHI.Reflect/BufferViewDescriptor.h>
+
+#include <Atom/RPI.Public/MeshDrawPacket.h>
+#include <Atom/RPI.Public/View.h>
+#include <Atom/RPI.Public/Buffer/Buffer.h>
+#include <Atom/RPI.Public/Model/Model.h>
+#include <Atom/RPI.Public/Shader/ShaderResourceGroup.h>
+
+#include <Atom/RPI.Reflect/Buffer/BufferAsset.h>
+#include <Atom/RPI.Reflect/Buffer/BufferAssetCreator.h>
+#include <Atom/RPI.Reflect/Model/ModelAssetCreator.h>
+#include <Atom/RPI.Reflect/Model/ModelLodAssetCreator.h>
+
+#include <Atom/Feature/RenderCommon.h>
+
+namespace Terrain
+{
+    namespace
+    {
+        [[maybe_unused]] static const char* TerrainMeshManagerName = "TerrainMeshManager";
+    }
+
+    namespace ShaderInputs
+    {
+        static const char* const PatchData("m_patchData");
+    }
+
+    void TerrainMeshManager::Initialize()
+    {
+        if (!InitializePatchModel())
+        {
+            AZ_Error(TerrainMeshManagerName, false, "Failed to create Terrain render buffers!");
+            return;
+        }
+        
+        OnTerrainDataChanged(AZ::Aabb::CreateNull(), TerrainDataChangedMask::HeightData);
+        AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusConnect();
+
+        m_isInitialized = true;
+    }
+    
+    bool TerrainMeshManager::IsInitialized() const
+    {
+        return m_isInitialized;
+    }
+
+    void TerrainMeshManager::Reset()
+    {
+        AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusDisconnect();
+        m_patchModel = {};
+        m_sectorData.clear();
+        m_rebuildSectors = true;
+        m_isInitialized = false;
+    }
+    
+    bool TerrainMeshManager::CheckRebuildSurfaces(MaterialInstance materialInstance, AZ::RPI::Scene& parentScene)
+    {
+        if (!m_rebuildSectors)
+        {
+            return false;
+        }
+
+        m_rebuildSectors = false;
+        m_sectorData.clear();
+
+        const auto layout = materialInstance->GetAsset()->GetObjectSrgLayout();
+
+        AZ::RHI::ShaderInputConstantIndex patchDataIndex = layout->FindShaderInputConstantIndex(AZ::Name(ShaderInputs::PatchData));
+        AZ_Error(TerrainMeshManagerName, patchDataIndex.IsValid(), "Failed to find shader input constant %s.", ShaderInputs::PatchData);
+
+        const float xFirstPatchStart = AZStd::floorf(m_worldBounds.GetMin().GetX() / GridMeters) * GridMeters;
+        const float xLastPatchStart = AZStd::floorf(m_worldBounds.GetMax().GetX() / GridMeters) * GridMeters;
+        const float yFirstPatchStart = AZStd::floorf(m_worldBounds.GetMin().GetY() / GridMeters) * GridMeters;
+        const float yLastPatchStart = AZStd::floorf(m_worldBounds.GetMax().GetY() / GridMeters) * GridMeters;
+            
+        const auto& materialAsset = materialInstance->GetAsset();
+        const auto& shaderAsset = materialAsset->GetMaterialTypeAsset()->GetShaderAssetForObjectSrg();
+
+        for (float yPatch = yFirstPatchStart; yPatch <= yLastPatchStart; yPatch += GridMeters)
+        {
+            for (float xPatch = xFirstPatchStart; xPatch <= xLastPatchStart; xPatch += GridMeters)
+            {
+                ShaderTerrainData objectSrgData;
+                objectSrgData.m_xyTranslation = { xPatch, yPatch };
+
+                m_sectorData.push_back();
+                SectorData& sectorData = m_sectorData.back();
+
+                for (auto& lod : m_patchModel->GetLods())
+                {
+                    objectSrgData.m_xyScale = m_sampleSpacing * GridSize;
+                        
+                    auto objectSrg = AZ::RPI::ShaderResourceGroup::Create(shaderAsset, materialAsset->GetObjectSrgLayout()->GetName());
+                    if (!objectSrg)
+                    {
+                        AZ_WarningOnce(TerrainMeshManagerName, false, "Failed to create a new shader resource group, skipping.");
+                        continue;
+                    }
+                    objectSrg->SetConstant(patchDataIndex, objectSrgData);
+                    objectSrg->Compile();
+
+                    AZ::RPI::ModelLod& modelLod = *lod.get();
+                    sectorData.m_drawPackets.emplace_back(modelLod, 0, materialInstance, objectSrg);
+                    AZ::RPI::MeshDrawPacket& drawPacket = sectorData.m_drawPackets.back();
+                        
+                    sectorData.m_srgs.emplace_back(objectSrg);
+
+                    // set the shader option to select forward pass IBL specular if necessary
+                    if (!drawPacket.SetShaderOption(AZ::Name("o_meshUseForwardPassIBLSpecular"), AZ::RPI::ShaderOptionValue{ false }))
+                    {
+                        AZ_Warning(TerrainMeshManagerName, false, "Failed to set o_meshUseForwardPassIBLSpecular on mesh draw packet");
+                    }
+                    const uint8_t stencilRef = AZ::Render::StencilRefs::UseDiffuseGIPass | AZ::Render::StencilRefs::UseIBLSpecularPass;
+                    drawPacket.SetStencilRef(stencilRef);
+                    drawPacket.Update(parentScene, true);
+                }
+
+                sectorData.m_aabb =
+                    AZ::Aabb::CreateFromMinMax(
+                        AZ::Vector3(xPatch, yPatch, m_worldBounds.GetMin().GetZ()),
+                        AZ::Vector3(xPatch + GridMeters, yPatch + GridMeters, m_worldBounds.GetMax().GetZ())
+                    );
+            }
+        }
+        return true;
+    }
+    
+    void TerrainMeshManager::DrawMeshes(const AZ::RPI::FeatureProcessor::RenderPacket& process)
+    {
+        for (auto& sectorData : m_sectorData)
+        {
+            uint8_t lodChoice = AZ::RPI::ModelLodAsset::LodCountMax;
+
+            // Go through all cameras and choose an LOD based on the closest camera.
+            for (auto& view : process.m_views)
+            {
+                if ((view->GetUsageFlags() & AZ::RPI::View::UsageFlags::UsageCamera) > 0)
+                {
+                    const AZ::Vector3 cameraPosition = view->GetCameraTransform().GetTranslation();
+                    const AZ::Vector2 cameraPositionXY = AZ::Vector2(cameraPosition.GetX(), cameraPosition.GetY());
+                    const AZ::Vector2 sectorCenterXY = AZ::Vector2(sectorData.m_aabb.GetCenter().GetX(), sectorData.m_aabb.GetCenter().GetY());
+
+                    const float sectorDistance = sectorCenterXY.GetDistance(cameraPositionXY);
+
+                    // This will be configurable later
+                    const float minDistanceForLod0 = (GridMeters * 4.0f);
+
+                    // For every distance doubling beyond a minDistanceForLod0, we only need half the mesh density. Each LOD
+                    // is exactly half the resolution of the last.
+                    const float lodForCamera = AZStd::floorf(AZ::GetMax(0.0f, log2f(sectorDistance / minDistanceForLod0)));
+
+                    // All cameras should render the same LOD so effects like shadows are consistent.
+                    lodChoice = AZ::GetMin(lodChoice, aznumeric_cast<uint8_t>(lodForCamera));
+                }
+            }
+
+            // Add the correct LOD draw packet for visible sectors.
+            for (auto& view : process.m_views)
+            {
+                AZ::Frustum viewFrustum = AZ::Frustum::CreateFromMatrixColumnMajor(view->GetWorldToClipMatrix());
+                if (viewFrustum.IntersectAabb(sectorData.m_aabb) != AZ::IntersectResult::Exterior)
+                {
+                    const uint8_t lodToRender = AZ::GetMin(lodChoice, aznumeric_cast<uint8_t>(sectorData.m_drawPackets.size() - 1));
+                    view->AddDrawPacket(sectorData.m_drawPackets.at(lodToRender).GetRHIDrawPacket());
+                }
+            }
+        }
+    }
+
+    void TerrainMeshManager::RebuildDrawPackets(AZ::RPI::Scene& scene)
+    {
+        for (auto& sectorData : m_sectorData)
+        {
+            for (auto& drawPacket : sectorData.m_drawPackets)
+            {
+                drawPacket.Update(scene, true);
+            }
+        }
+    }
+
+    void TerrainMeshManager::OnTerrainDataDestroyBegin()
+    {
+        Reset();
+    }
+    
+    void TerrainMeshManager::OnTerrainDataChanged([[maybe_unused]] const AZ::Aabb& dirtyRegion, TerrainDataChangedMask dataChangedMask)
+    {
+        if ((dataChangedMask & (TerrainDataChangedMask::HeightData | TerrainDataChangedMask::Settings)) != 0)
+        {
+            AZ::Aabb worldBounds = AZ::Aabb::CreateNull();
+            AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
+                worldBounds, &AzFramework::Terrain::TerrainDataRequests::GetTerrainAabb);
+            
+            AZ::Vector2 queryResolution2D = AZ::Vector2(1.0f);
+            AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
+                queryResolution2D, &AzFramework::Terrain::TerrainDataRequests::GetTerrainHeightQueryResolution);
+            // Currently query resolution is multidimensional but the rendering system only supports this changing in one dimension.
+            float queryResolution = queryResolution2D.GetX();
+
+            // Sectors need to be rebuilt if the world bounds change in the x/y, or the sample spacing changes.
+            m_rebuildSectors = m_rebuildSectors ||
+                m_worldBounds.GetMin().GetX() != worldBounds.GetMin().GetX() ||
+                m_worldBounds.GetMin().GetY() != worldBounds.GetMin().GetY() ||
+                m_worldBounds.GetMax().GetX() != worldBounds.GetMax().GetX() ||
+                m_worldBounds.GetMax().GetY() != worldBounds.GetMax().GetY() ||
+                m_sampleSpacing != queryResolution;
+
+            m_worldBounds = worldBounds;
+            m_sampleSpacing = queryResolution;
+        }
+    }
+
+    void TerrainMeshManager::InitializeTerrainPatch(uint16_t gridSize, PatchData& patchdata)
+    {
+        patchdata.m_positions.clear();
+        patchdata.m_indices.clear();
+
+        const uint16_t gridVertices = gridSize + 1; // For m_gridSize quads, (m_gridSize + 1) vertices are needed.
+        const size_t size = gridVertices * gridVertices;
+
+        patchdata.m_positions.reserve(size);
+
+        for (uint16_t y = 0; y < gridVertices; ++y)
+        {
+            for (uint16_t x = 0; x < gridVertices; ++x)
+            {
+                patchdata.m_positions.push_back({ aznumeric_cast<float>(x) / gridSize, aznumeric_cast<float>(y) / gridSize });
+            }
+        }
+
+        patchdata.m_indices.reserve(gridSize * gridSize * 6); // total number of quads, 2 triangles with 6 indices per quad.
+        
+        for (uint16_t y = 0; y < gridSize; ++y)
+        {
+            for (uint16_t x = 0; x < gridSize; ++x)
+            {
+                const uint16_t topLeft = y * gridVertices + x;
+                const uint16_t topRight = topLeft + 1;
+                const uint16_t bottomLeft = (y + 1) * gridVertices + x;
+                const uint16_t bottomRight = bottomLeft + 1;
+
+                patchdata.m_indices.emplace_back(topLeft);
+                patchdata.m_indices.emplace_back(topRight);
+                patchdata.m_indices.emplace_back(bottomLeft);
+                patchdata.m_indices.emplace_back(bottomLeft);
+                patchdata.m_indices.emplace_back(topRight);
+                patchdata.m_indices.emplace_back(bottomRight);
+            }
+        }
+    }
+    
+    AZ::Outcome<AZ::Data::Asset<AZ::RPI::BufferAsset>> TerrainMeshManager::CreateBufferAsset(
+        const void* data, const AZ::RHI::BufferViewDescriptor& bufferViewDescriptor, const AZStd::string& bufferName)
+    {
+        AZ::RPI::BufferAssetCreator creator;
+        creator.Begin(AZ::Uuid::CreateRandom());
+
+        AZ::RHI::BufferDescriptor bufferDescriptor;
+        bufferDescriptor.m_bindFlags = AZ::RHI::BufferBindFlags::InputAssembly | AZ::RHI::BufferBindFlags::ShaderRead;
+        bufferDescriptor.m_byteCount = static_cast<uint64_t>(bufferViewDescriptor.m_elementSize) * static_cast<uint64_t>(bufferViewDescriptor.m_elementCount);
+
+        creator.SetBuffer(data, bufferDescriptor.m_byteCount, bufferDescriptor);
+        creator.SetBufferViewDescriptor(bufferViewDescriptor);
+        creator.SetUseCommonPool(AZ::RPI::CommonBufferPoolType::StaticInputAssembly);
+
+        AZ::Data::Asset<AZ::RPI::BufferAsset> bufferAsset;
+        if (creator.End(bufferAsset))
+        {
+            bufferAsset.SetHint(bufferName);
+            return AZ::Success(bufferAsset);
+        }
+
+        return AZ::Failure();
+    }
+
+    bool TerrainMeshManager::InitializePatchModel()
+    {
+        AZ::RPI::ModelAssetCreator modelAssetCreator;
+        modelAssetCreator.Begin(AZ::Uuid::CreateRandom());
+
+        uint16_t gridSize = GridSize;
+        float gridSpacing = GridSpacing;
+
+        for (uint32_t i = 0; i < AZ::RPI::ModelLodAsset::LodCountMax && gridSize > 0; ++i)
+        {
+            PatchData patchData;
+            InitializeTerrainPatch(gridSize, patchData);
+
+            const auto positionBufferViewDesc = AZ::RHI::BufferViewDescriptor::CreateTyped(0, aznumeric_cast<uint32_t>(patchData.m_positions.size()), AZ::RHI::Format::R32G32_FLOAT);
+            const auto positionsOutcome = CreateBufferAsset(patchData.m_positions.data(), positionBufferViewDesc, "TerrainPatchPositions");
+        
+            const auto indexBufferViewDesc = AZ::RHI::BufferViewDescriptor::CreateTyped(0, aznumeric_cast<uint32_t>(patchData.m_indices.size()), AZ::RHI::Format::R16_UINT);
+            const auto indicesOutcome = CreateBufferAsset(patchData.m_indices.data(), indexBufferViewDesc, "TerrainPatchIndices");
+
+            if (!positionsOutcome.IsSuccess() || !indicesOutcome.IsSuccess())
+            {
+                AZ_Error(TerrainMeshManagerName, false, "Failed to create GPU buffers for Terrain");
+                return false;
+            }
+            
+            AZ::RPI::ModelLodAssetCreator modelLodAssetCreator;
+            modelLodAssetCreator.Begin(AZ::Uuid::CreateRandom());
+
+            modelLodAssetCreator.BeginMesh();
+            modelLodAssetCreator.AddMeshStreamBuffer(AZ::RHI::ShaderSemantic{ "POSITION" }, AZ::Name(), {positionsOutcome.GetValue(), positionBufferViewDesc});
+            modelLodAssetCreator.SetMeshIndexBuffer({indicesOutcome.GetValue(), indexBufferViewDesc});
+
+            AZ::Aabb aabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3(0.0, 0.0, 0.0), AZ::Vector3(GridMeters, GridMeters, 0.0));
+            modelLodAssetCreator.SetMeshAabb(AZStd::move(aabb));
+            modelLodAssetCreator.SetMeshName(AZ::Name("Terrain Patch"));
+            modelLodAssetCreator.EndMesh();
+
+            AZ::Data::Asset<AZ::RPI::ModelLodAsset> modelLodAsset;
+            modelLodAssetCreator.End(modelLodAsset);
+        
+            modelAssetCreator.AddLodAsset(AZStd::move(modelLodAsset));
+
+            gridSize = gridSize / 2;
+            gridSpacing *= 2.0f;
+        }
+
+        AZ::Data::Asset<AZ::RPI::ModelAsset> modelAsset;
+        bool success = modelAssetCreator.End(modelAsset);
+
+        m_patchModel = AZ::RPI::Model::FindOrCreate(modelAsset);
+
+        return success;
+    }
+    
+    template<typename Callback>
+    void TerrainMeshManager::ForOverlappingSectors(const AZ::Aabb& bounds, Callback callback)
+    {
+        for (SectorData& sectorData : m_sectorData)
+        {
+            if (sectorData.m_aabb.Overlaps(bounds))
+            {
+                callback(sectorData);
+            }
+        }
+    }
+
+}

+ 117 - 0
Gems/Terrain/Code/Source/TerrainRenderer/TerrainMeshManager.h

@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <AzCore/base.h>
+#include <AzCore/Math/Aabb.h>
+#include <AzCore/Outcome/Outcome.h>
+#include <AzCore/std/containers/vector.h>
+
+#include <AtomCore/Instance/Instance.h>
+
+#include <AzFramework/Terrain/TerrainDataRequestBus.h>
+
+#include <Atom/RPI.Public/Model/Model.h>
+#include <Atom/RPI.Public/FeatureProcessor.h>
+#include <Atom/RPI.Public/MeshDrawPacket.h>
+
+#include <Atom/RPI.Reflect/Model/ModelLodAsset.h>
+
+
+namespace AZ::RPI
+{
+    class BufferAsset;
+}
+
+namespace AZ::RHI
+{
+    struct BufferViewDescriptor;
+}
+
+namespace Terrain
+{
+    class TerrainMeshManager
+        : private AzFramework::Terrain::TerrainDataNotificationBus::Handler
+    {
+    private:
+        
+        using MaterialInstance = AZ::Data::Instance<AZ::RPI::Material>;
+
+    public:
+
+        AZ_RTTI(TerrainMeshManager, "{62C84AD8-05FE-4C78-8501-A2DB6731B9B7}");
+        AZ_DISABLE_COPY_MOVE(TerrainMeshManager);
+        
+        TerrainMeshManager() = default;
+        ~TerrainMeshManager() = default;
+
+        void Initialize();
+        bool IsInitialized() const;
+        void Reset();
+
+        bool CheckRebuildSurfaces(MaterialInstance materialInstance, AZ::RPI::Scene& parentScene);
+        void DrawMeshes(const AZ::RPI::FeatureProcessor::RenderPacket& process);
+        void RebuildDrawPackets(AZ::RPI::Scene& scene);
+
+        static constexpr float GridSpacing{ 1.0f };
+        static constexpr int32_t GridSize{ 64 }; // number of terrain quads (vertices are m_gridSize + 1)
+        static constexpr float GridMeters{ GridSpacing * GridSize };
+        static constexpr uint32_t MaxMaterialsPerSector = 4;
+
+    private:
+        
+        struct VertexPosition
+        {
+            float m_posx;
+            float m_posy;
+        };
+
+        struct PatchData
+        {
+            AZStd::vector<VertexPosition> m_positions;
+            AZStd::vector<uint16_t> m_indices;
+        };
+        
+        struct SectorData
+        {
+            AZStd::fixed_vector<AZ::RPI::MeshDrawPacket, AZ::RPI::ModelLodAsset::LodCountMax> m_drawPackets;
+            AZStd::fixed_vector<AZ::Data::Instance<AZ::RPI::ShaderResourceGroup>, AZ::RPI::ModelLodAsset::LodCountMax> m_srgs; // Hold on to refs so it's not dropped
+            AZ::Aabb m_aabb;
+        };
+        
+        struct ShaderTerrainData // Must align with struct in Object Srg
+        {
+            AZStd::array<float, 2> m_xyTranslation{ 0.0f, 0.0f };
+            float m_xyScale{ 1.0f };
+        };
+
+        // AzFramework::Terrain::TerrainDataNotificationBus overrides...
+        void OnTerrainDataDestroyBegin() override;
+        void OnTerrainDataChanged(const AZ::Aabb& dirtyRegion, TerrainDataChangedMask dataChangedMask) override;
+
+        AZ::Outcome<AZ::Data::Asset<AZ::RPI::BufferAsset>> CreateBufferAsset(
+            const void* data, const AZ::RHI::BufferViewDescriptor& bufferViewDescriptor, const AZStd::string& bufferName);
+
+        void InitializeTerrainPatch(uint16_t gridSize, PatchData& patchdata);
+        bool InitializePatchModel();
+        
+        template<typename Callback>
+        void ForOverlappingSectors(const AZ::Aabb& bounds, Callback callback);
+
+        AZStd::vector<SectorData> m_sectorData;
+        AZ::Data::Instance<AZ::RPI::Model> m_patchModel;
+        
+        AZ::Aabb m_worldBounds{ AZ::Aabb::CreateNull() };
+        float m_sampleSpacing = 1.0f;
+
+        bool m_isInitialized{ false };
+        bool m_rebuildSectors{ true };
+
+    };
+}

+ 41 - 0
Gems/Terrain/Code/Source/TerrainRenderer/Vector2i.cpp

@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <TerrainRenderer/Vector2i.h>
+
+namespace Terrain
+{
+    auto Vector2i::operator+(const Vector2i& rhs) const -> Vector2i
+    {
+        Vector2i offsetPoint = *this;
+        offsetPoint += rhs;
+        return offsetPoint;
+    }
+    
+    auto Vector2i::operator+=(const Vector2i& rhs) -> Vector2i&
+    {
+        m_x += rhs.m_x;
+        m_y += rhs.m_y;
+        return *this;
+    }
+
+    auto Vector2i::operator-(const Vector2i& rhs) const -> Vector2i
+    {
+        return *this + -rhs;
+    }
+    
+    auto Vector2i::operator-=(const Vector2i& rhs) -> Vector2i&
+    {
+        return *this += -rhs;
+    }
+    
+    auto Vector2i::operator-() const -> Vector2i
+    {
+        return {-m_x, -m_y};
+    }
+}

+ 29 - 0
Gems/Terrain/Code/Source/TerrainRenderer/Vector2i.h

@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <AzCore/base.h>
+
+namespace Terrain
+{
+    class Vector2i
+    {
+    public:
+
+        Vector2i operator+(const Vector2i& rhs) const;
+        Vector2i& operator+=(const Vector2i& rhs);
+        Vector2i operator-(const Vector2i& rhs) const;
+        Vector2i& operator-=(const Vector2i& rhs);
+        Vector2i operator-() const;
+        
+        int32_t m_x{ 0 };
+        int32_t m_y{ 0 };
+
+    };
+}

+ 12 - 0
Gems/Terrain/Code/terrain_files.cmake

@@ -31,11 +31,23 @@ set(FILES
     Source/TerrainRenderer/Components/TerrainSurfaceMaterialsListComponent.h
     Source/TerrainRenderer/Components/TerrainMacroMaterialComponent.cpp
     Source/TerrainRenderer/Components/TerrainMacroMaterialComponent.h
+    Source/TerrainRenderer/Aabb2i.cpp
+    Source/TerrainRenderer/Aabb2i.h
     Source/TerrainRenderer/TerrainFeatureProcessor.cpp
     Source/TerrainRenderer/TerrainFeatureProcessor.h
+    Source/TerrainRenderer/TerrainDetailMaterialManager.cpp
+    Source/TerrainRenderer/TerrainDetailMaterialManager.h
+    Source/TerrainRenderer/TerrainMacroMaterialManager.cpp
+    Source/TerrainRenderer/TerrainMacroMaterialManager.h
+    Source/TerrainRenderer/TerrainMeshManager.cpp
+    Source/TerrainRenderer/TerrainMeshManager.h
+    Source/TerrainRenderer/BindlessImageArrayHandler.cpp
+    Source/TerrainRenderer/BindlessImageArrayHandler.h
     Source/TerrainRenderer/TerrainAreaMaterialRequestBus.h
     Source/TerrainRenderer/TerrainMacroMaterialBus.cpp
     Source/TerrainRenderer/TerrainMacroMaterialBus.h
+    Source/TerrainRenderer/Vector2i.cpp
+    Source/TerrainRenderer/Vector2i.h
     Source/TerrainSystem/TerrainSystem.cpp
     Source/TerrainSystem/TerrainSystem.h
     Source/TerrainSystem/TerrainSystemBus.h

Some files were not shown because too many files changed in this diff