diff --git a/Gems/Atom/Feature/Common/Assets/ShaderResourceGroups/SceneSrgAll.azsli b/Gems/Atom/Feature/Common/Assets/ShaderResourceGroups/SceneSrgAll.azsli index 421178bb01..4480524468 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderResourceGroups/SceneSrgAll.azsli +++ b/Gems/Atom/Feature/Common/Assets/ShaderResourceGroups/SceneSrgAll.azsli @@ -13,4 +13,5 @@ #ifdef AZ_COLLECTING_PARTIAL_SRGS #include #include +#include // Temporary until gem partial view srgs can be included automatically. #endif diff --git a/Gems/Atom/Feature/Common/Assets/ShaderResourceGroups/ViewSrgAll.azsli b/Gems/Atom/Feature/Common/Assets/ShaderResourceGroups/ViewSrgAll.azsli index 9253885cfc..3e90e1a441 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderResourceGroups/ViewSrgAll.azsli +++ b/Gems/Atom/Feature/Common/Assets/ShaderResourceGroups/ViewSrgAll.azsli @@ -12,5 +12,4 @@ #ifdef AZ_COLLECTING_PARTIAL_SRGS #include -#include // Temporary until gem partial view srgs can be included automatically. #endif diff --git a/Gems/Atom/Feature/Common/Code/Source/Utils/GpuBufferHandler.cpp b/Gems/Atom/Feature/Common/Code/Source/Utils/GpuBufferHandler.cpp index 7a2827bebe..65bdf655a7 100644 --- a/Gems/Atom/Feature/Common/Code/Source/Utils/GpuBufferHandler.cpp +++ b/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()) diff --git a/Gems/Terrain/Assets/Materials/Terrain/DefaultPbrTerrain.material b/Gems/Terrain/Assets/Materials/Terrain/DefaultPbrTerrain.material index 53f6fd5b9e..da2fc95293 100644 --- a/Gems/Terrain/Assets/Materials/Terrain/DefaultPbrTerrain.material +++ b/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 ] } } } diff --git a/Gems/Terrain/Assets/Materials/Terrain/PbrTerrain.materialtype b/Gems/Terrain/Assets/Materials/Terrain/PbrTerrain.materialtype index 1e7305cc11..836e85661d 100644 --- a/Gems/Terrain/Assets/Materials/Terrain/PbrTerrain.materialtype +++ b/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" diff --git a/Gems/Terrain/Assets/Shaders/Terrain/SceneSrg.azsli b/Gems/Terrain/Assets/Shaders/Terrain/SceneSrg.azsli new file mode 100644 index 0000000000..2545db7c93 --- /dev/null +++ b/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; +} diff --git a/Gems/Terrain/Assets/Shaders/Terrain/TerrainCommon.azsli b/Gems/Terrain/Assets/Shaders/Terrain/TerrainCommon.azsli index 770f877ea8..ee3e9b2a5f 100644 --- a/Gems/Terrain/Assets/Shaders/Terrain/TerrainCommon.azsli +++ b/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. + float3 m_worldMin; + float3 m_worldMax; + float2 m_xyPosition; - // 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; + float2 m_textureSize; + float2 m_rcpTextureSize; + float2 m_sampleSpacing; + float2 m_rcpSampleSpacing; - // Offset from center position to texel - float2 f = texelPos - tc1; + float m_heightScale; + int2 m_heightmapCoord; - // 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; -} - -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; -} - -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 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)); - } - else - { - height = ViewSrg::m_heightmapImage.SampleLevel(ViewSrg::HeightmapSampler, uv, 0).r; + // 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; } - return ObjectSrg::m_terrainData.m_heightScale * (height - 0.5f); -} - -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)) + float2 GetWorldXYPosition(in ObjectSrg::PatchData patchData, in float2 vertexPosition) { - return asfloat(0x7fc00000); // NaN + return float2(patchData.m_xyTranslation + vertexPosition * patchData.m_xyScale); } - // Loop up the height and calculate our final position. - float height = GetHeight(uv); - return mul(GetObject_WorldMatrix(), float4(vertexPosition, height, 1.0f)).xyz; -} + float2 GetHeightmapUv(in float2 position, in float2 worldMin, in float2 worldMax) + { + return (position - worldMin) / (worldMax - worldMin); + } -float4 GetTerrainProjectedPosition(ObjectSrg::TerrainData terrainData, float2 vertexPosition, float2 uv) -{ - return mul(ViewSrg::m_viewProjectionMatrix, float4(GetTerrainWorldPosition(terrainData, vertexPosition, uv), 1.0)); -} + int2 GetHeightmapCoord(in float2 position, in float2 rcpSampleSpacing, in float2 worldMin) + { + return int2((position - worldMin) * rcpSampleSpacing); + } + + 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; + } + + 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); + } +}; diff --git a/Gems/Terrain/Assets/Shaders/Terrain/TerrainDetailHelpers.azsli b/Gems/Terrain/Assets/Shaders/Terrain/TerrainDetailHelpers.azsli index 33c817c0f9..b18f2885db 100644 --- a/Gems/Terrain/Assets/Shaders/Terrain/TerrainDetailHelpers.azsli +++ b/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; diff --git a/Gems/Terrain/Assets/Shaders/Terrain/TerrainPBR_ForwardPass.azsl b/Gems/Terrain/Assets/Shaders/Terrain/TerrainPBR_ForwardPass.azsl index ab9064e740..741f45d775 100644 --- a/Gems/Terrain/Assets/Shaders/Terrain/TerrainPBR_ForwardPass.azsl +++ b/Gems/Terrain/Assets/Shaders/Terrain/TerrainPBR_ForwardPass.azsl @@ -8,6 +8,7 @@ #include +#include #include #include #include @@ -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) { - 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; - } + 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) + { + 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 ------- diff --git a/Gems/Terrain/Assets/Shaders/Terrain/TerrainSrg.azsli b/Gems/Terrain/Assets/Shaders/Terrain/TerrainSrg.azsli index f980e02b9d..3fcf8d75ca 100644 --- a/Gems/Terrain/Assets/Shaders/Terrain/TerrainSrg.azsli +++ b/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 m_detailMaterialIdImage; StructuredBuffer m_detailMaterialData; - Texture2D m_detailTextures[]; // bindless array of all textures for detail materials + StructuredBuffer 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; diff --git a/Gems/Terrain/Assets/Shaders/Terrain/Terrain_DepthPass.azsl b/Gems/Terrain/Assets/Shaders/Terrain/Terrain_DepthPass.azsl index fd855a1bcd..c74a0646ee 100644 --- a/Gems/Terrain/Assets/Shaders/Terrain/Terrain_DepthPass.azsl +++ b/Gems/Terrain/Assets/Shaders/Terrain/Terrain_DepthPass.azsl @@ -6,6 +6,7 @@ */ #include +#include #include #include "TerrainCommon.azsli" #include @@ -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; } diff --git a/Gems/Terrain/Assets/Shaders/Terrain/ViewSrg.azsli b/Gems/Terrain/Assets/Shaders/Terrain/ViewSrg.azsli deleted file mode 100644 index 144f2abf6b..0000000000 --- a/Gems/Terrain/Assets/Shaders/Terrain/ViewSrg.azsli +++ /dev/null @@ -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 m_detailMaterialIdImage; - StructuredBuffer m_detailMaterialData; - - Texture2D m_detailTextures[]; // bindless array of all textures for detail materials - - float2 m_detailMaterialIdImageCenter; - float m_detailHalfPixelUv; - - float4 m_detailAabb; -} diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/Aabb2i.cpp b/Gems/Terrain/Code/Source/TerrainRenderer/Aabb2i.cpp new file mode 100644 index 0000000000..c38651f296 --- /dev/null +++ b/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 +#include + +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; + } +} diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/Aabb2i.h b/Gems/Terrain/Code/Source/TerrainRenderer/Aabb2i.h new file mode 100644 index 0000000000..88f735564d --- /dev/null +++ b/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 +#include + +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::min(), AZStd::numeric_limits::min()}; + Vector2i m_max{AZStd::numeric_limits::max(), AZStd::numeric_limits::max()}; + + }; +} diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/BindlessImageArrayHandler.cpp b/Gems/Terrain/Code/Source/TerrainRenderer/BindlessImageArrayHandler.cpp new file mode 100644 index 0000000000..595877c88b --- /dev/null +++ b/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 +#include +#include + +namespace AZ::Render +{ + namespace + { + [[maybe_unused]] const char* BindlessImageArrayHandlerName = "TerrainFeatureProcessor"; + } + + void BindlessImageArrayHandler::Initialize(AZ::Data::Instance& 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& 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 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(m_bindlessImageViews.size()); + m_bindlessImageViews.push_back(imageView); + } + return imageIndex; + } + + void BindlessImageArrayHandler::UpdateBindlessImage(uint16_t index, const AZ::RHI::ImageView* imageView) + { + AZStd::shared_lock lock(m_updateMutex); + m_bindlessImageViews.at(index) = imageView; + } + + void BindlessImageArrayHandler::RemoveBindlessImage(uint16_t index) + { + AZStd::unique_lock 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& srg) const + { + if (!m_isInitialized) + { + AZ_Error("BindlessImageArrayHandler", false, "BindlessImageArrayHandler not initialized") + return false; + } + + AZStd::array_view imageViews(m_bindlessImageViews.data(), m_bindlessImageViews.size()); + return srg->SetImageViewUnboundedArray(m_texturesIndex, imageViews); + } + +} diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/BindlessImageArrayHandler.h b/Gems/Terrain/Code/Source/TerrainRenderer/BindlessImageArrayHandler.h new file mode 100644 index 0000000000..5315e8526d --- /dev/null +++ b/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 + +#include +#include +#include + +namespace AZ::Render +{ + class BindlessImageArrayHandler + { + public: + + static constexpr uint16_t InvalidImageIndex = 0xFFFF; + + BindlessImageArrayHandler() = default; + ~BindlessImageArrayHandler() = default; + + void Initialize(AZ::Data::Instance& srg, const AZ::Name& propertyName); + void Reset(); + bool IsInitialized() const; + bool UpdateSrgIndices(AZ::Data::Instance& 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& srg) const; + + private: + + AZStd::vector m_bindlessImageViews; + AZStd::vector m_bindlessImageViewFreeList; + RHI::ShaderInputImageUnboundedArrayIndex m_texturesIndex; + AZStd::shared_mutex m_updateMutex; + bool m_isInitialized{ false }; + }; +} + diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainDetailMaterialManager.cpp b/Gems/Terrain/Code/Source/TerrainRenderer/TerrainDetailMaterialManager.cpp new file mode 100644 index 0000000000..2bc9e42bff --- /dev/null +++ b/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 +#include + +#include + +#include +#include +#include + +#include + +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& bindlessImageHandler, + AZ::Data::Instance& 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 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& 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::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& terrainSrg) + { + if (m_detailMaterialBufferNeedsUpdate) + { + m_detailMaterialBufferNeedsUpdate = false; + m_detailMaterialDataBuffer.UpdateBuffer(m_detailMaterialShaderData.GetRawData(), aznumeric_cast(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(AZStd::roundf(cameraPosition.GetX() / DetailTextureScale)); + int32_t newDetailTexturePosY = aznumeric_cast(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(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() expects the actaul type, not a reference type, so the reference needs to be removed. + using TypeRefRemoved = AZStd::remove_cvref_t; + ref = material->GetPropertyValue(index).GetValue(); + } + }; + + auto applyImage = [&](const char* const indexName, AZ::Data::Instance& 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(); + } + if (index.IsValid() && useTextureValue) + { + ref = material->GetPropertyValue(index).GetValue>(); + } + 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(); + 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(); + 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(); + 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 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(AZStd::roundf(clampedCoverage.GetMin().GetX() / DetailTextureScale)); + updateBounds.m_min.m_y = aznumeric_cast(AZStd::roundf(clampedCoverage.GetMin().GetY() / DetailTextureScale)); + updateBounds.m_max.m_x = aznumeric_cast(AZStd::roundf(clampedCoverage.GetMax().GetX() / DetailTextureScale)); + updateBounds.m_max.m_y = aznumeric_cast(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 textureSpaceAreas; + AZStd::array 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 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(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(materialId); + float totalWeight = firstWeight + surfaceTagWeight.m_weight; + float blendWeight = 1.0f - (firstWeight / totalWeight); + pixels.at(index).m_blend = aznumeric_cast(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(left); + imageUpdateRequest.m_imageSubresourcePixelOffset.m_top = aznumeric_cast(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& textureSpaceAreas, AZStd::array& 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& 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& container) + -> DetailMaterialListRegion& + { + DetailMaterialListRegion* dataPtr = FindByEntityId(entityId, container); + if (dataPtr != nullptr) + { + return *dataPtr; + } + + const uint16_t slotId = container.GetFreeSlotIndex(); + AZ_Assert(slotId != AZ::Render::IndexedDataVector::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& container) + { + for (DetailMaterialListRegion& data : container.GetDataVector()) + { + if (data.m_entityId == entityId) + { + container.RemoveData(&data); + return; + } + } + AZ_Assert(false, "Entity Id not found in container.") + } + +} diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainDetailMaterialManager.h b/Gems/Terrain/Code/Source/TerrainRenderer/TerrainDetailMaterialManager.h new file mode 100644 index 0000000000..98f2c26dd8 --- /dev/null +++ b/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 +#include +#include + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +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& bindlessImageHandler, + AZ::Data::Instance& terrainSrg); + bool IsInitialized() const; + void Reset(); + bool UpdateSrgIndices(AZ::Data::Instance& srg); + + void Update(const AZ::Vector3& cameraPosition, AZ::Data::Instance& terrainSrg); + + private: + + using MaterialInstance = AZ::Data::Instance; + 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 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 m_colorImage; + AZ::Data::Instance m_normalImage; + AZ::Data::Instance m_roughnessImage; + AZ::Data::Instance m_metalnessImage; + AZ::Data::Instance m_specularF0Image; + AZ::Data::Instance m_occlusionImage; + AZ::Data::Instance 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 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& textureSpaceAreas, AZStd::array& scaledWorldSpaceAreas); + + DetailMaterialListRegion* FindByEntityId(AZ::EntityId entityId, AZ::Render::IndexedDataVector& container); + DetailMaterialListRegion& FindOrCreateByEntityId(AZ::EntityId entityId, AZ::Render::IndexedDataVector& container); + void RemoveByEntityId(AZ::EntityId entityId, AZ::Render::IndexedDataVector& container); + + AZStd::shared_ptr m_bindlessImageHandler; + + AZ::Data::Instance m_detailTextureImage; + + AZ::Render::IndexedDataVector m_detailMaterials; + AZ::Render::IndexedDataVector m_detailMaterialRegions; + AZ::Render::SparseVector m_detailMaterialShaderData; + AZ::Render::GpuBufferHandler m_detailMaterialDataBuffer; + + AZ::Aabb m_dirtyDetailRegion{ AZ::Aabb::CreateNull() }; + AZ::Vector3 m_previousCameraPosition = AZ::Vector3(AZStd::numeric_limits::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 }; + + }; +} diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.cpp b/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.cpp index 9f83844243..351c34308a 100644 --- a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.cpp +++ b/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.cpp @@ -7,38 +7,22 @@ */ #include -#include - -#include -#include -#include -#include -#include #include -#include -#include -#include #include #include #include #include -#include -#include #include #include -#include #include #include #include #include #include -#include -#include -#include #include @@ -50,79 +34,19 @@ namespace Terrain { [[maybe_unused]] const char* TerrainFPName = "TerrainFeatureProcessor"; const char* TerrainHeightmapChars = "TerrainHeightmap"; - const char* TerrainDetailChars = "TerrainDetail"; } - namespace ViewSrgInputs + namespace SceneSrgInputs { static const char* const HeightmapImage("m_heightmapImage"); + static const char* const TerrainWorldData("m_terrainWorldData"); } 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"); - static const char* const DetailTextures("m_detailTextures"); + static const char* const Textures("m_textures"); } - 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 ShaderInputs - { - static const char* const ModelToWorld("m_modelToWorld"); - static const char* const TerrainData("m_terrainData"); - static const char* const MacroMaterialData("m_macroMaterialData"); - static const char* const MacroMaterialCount("m_macroMaterialCount"); - static const char* const MacroColorMap("m_macroColorMap"); - static const char* const MacroNormalMap("m_macroNormalMap"); - } - - 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." - ); - - void TerrainFeatureProcessor::Reflect(AZ::ReflectContext* context) { if (AZ::SerializeContext* serialize = azrtti_cast(context)) @@ -150,13 +74,17 @@ namespace Terrain void TerrainFeatureProcessor::Initialize() { - // Load indices for the View Srg. + m_meshManager.Initialize(); + m_imageArrayHandler = AZStd::make_shared(); - auto viewSrgLayout = AZ::RPI::RPISystemInterface::Get()->GetViewSrgLayout(); + auto sceneSrgLayout = AZ::RPI::RPISystemInterface::Get()->GetSceneSrgLayout(); - m_heightmapPropertyIndex = viewSrgLayout->FindShaderInputImageIndex(AZ::Name(ViewSrgInputs::HeightmapImage)); - AZ_Error(TerrainFPName, m_heightmapPropertyIndex.IsValid(), "Failed to find view srg input constant %s.", ViewSrgInputs::HeightmapImage); + m_heightmapPropertyIndex = sceneSrgLayout->FindShaderInputImageIndex(AZ::Name(SceneSrgInputs::HeightmapImage)); + AZ_Error(TerrainFPName, m_heightmapPropertyIndex.IsValid(), "Failed to find scene srg input constant %s.", SceneSrgInputs::HeightmapImage); + m_worldDataIndex = sceneSrgLayout->FindShaderInputConstantIndex(AZ::Name(SceneSrgInputs::TerrainWorldData)); + AZ_Error(TerrainFPName, m_worldDataIndex.IsValid(), "Failed to find scene srg input constant %s.", SceneSrgInputs::TerrainWorldData); + // Load the terrain material asynchronously const AZStd::string materialFilePath = "Materials/Terrain/DefaultPbrTerrain.azmaterial"; m_materialAssetLoader = AZStd::make_unique(); @@ -179,31 +107,24 @@ namespace Terrain } } ); - if (!InitializePatchModel()) - { - AZ_Error(TerrainFPName, false, "Failed to create Terrain render buffers!"); - return; - } OnTerrainDataChanged(AZ::Aabb::CreateNull(), TerrainDataChangedMask::HeightData); } void TerrainFeatureProcessor::Deactivate() { - TerrainMacroMaterialNotificationBus::Handler::BusDisconnect(); AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusDisconnect(); AZ::RPI::MaterialReloadNotificationBus::Handler::BusDisconnect(); DisableSceneNotification(); + OnTerrainDataDestroyBegin(); - m_patchModel = {}; - m_areaData = {}; - m_dirtyRegion = AZ::Aabb::CreateNull(); - m_sectorData.clear(); - m_macroMaterials.Clear(); m_materialAssetLoader = {}; m_materialInstance = {}; + m_meshManager.Reset(); + m_macroMaterialManager.Reset(); + m_detailMaterialManager.Reset(); } void TerrainFeatureProcessor::Render(const AZ::RPI::FeatureProcessor::RenderPacket& packet) @@ -213,7 +134,10 @@ namespace Terrain void TerrainFeatureProcessor::OnTerrainDataDestroyBegin() { - m_areaData = {}; + m_heightmapImage = {}; + m_terrainBounds = AZ::Aabb::CreateNull(); + m_dirtyRegion = AZ::Aabb::CreateNull(); + m_heightmapNeedsUpdate = false; } void TerrainFeatureProcessor::OnTerrainDataChanged(const AZ::Aabb& dirtyRegion, TerrainDataChangedMask dataChangedMask) @@ -222,10 +146,6 @@ namespace Terrain { TerrainHeightOrSettingsUpdated(dirtyRegion); } - if ((dataChangedMask & TerrainDataChangedMask::SurfaceData) != 0) - { - TerrainSurfaceDataUpdated(dirtyRegion); - } } void TerrainFeatureProcessor::TerrainHeightOrSettingsUpdated(const AZ::Aabb& dirtyRegion) @@ -239,131 +159,15 @@ namespace Terrain m_dirtyRegion.AddAabb(regionToUpdate); m_dirtyRegion.Clamp(worldBounds); - const AZ::Transform transform = AZ::Transform::CreateTranslation(worldBounds.GetCenter()); - 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_areaData.m_rebuildSectors = m_areaData.m_rebuildSectors || - m_areaData.m_terrainBounds.GetMin().GetX() != worldBounds.GetMin().GetX() || - m_areaData.m_terrainBounds.GetMin().GetY() != worldBounds.GetMin().GetY() || - m_areaData.m_terrainBounds.GetMax().GetX() != worldBounds.GetMax().GetX() || - m_areaData.m_terrainBounds.GetMax().GetY() != worldBounds.GetMax().GetY() || - m_areaData.m_sampleSpacing != queryResolution; - - m_areaData.m_transform = transform; - m_areaData.m_terrainBounds = worldBounds; - m_areaData.m_sampleSpacing = queryResolution; - m_areaData.m_heightmapUpdated = true; - } - - void TerrainFeatureProcessor::TerrainSurfaceDataUpdated(const AZ::Aabb& dirtyRegion) - { - m_dirtyDetailRegion.AddAabb(dirtyRegion); - } - - void TerrainFeatureProcessor::OnTerrainMacroMaterialCreated(AZ::EntityId entityId, const MacroMaterialData& newMaterialData) - { - MacroMaterialData& materialData = FindOrCreateByEntityId(entityId, m_macroMaterials); - - UpdateMacroMaterialData(materialData, newMaterialData); - - // Update all sectors in region. - ForOverlappingSectors(materialData.m_bounds, - [&](SectorData& sectorData) { - if (sectorData.m_macroMaterials.size() < sectorData.m_macroMaterials.max_size()) - { - sectorData.m_macroMaterials.push_back(m_macroMaterials.GetIndexForData(&materialData)); - } - } - ); - } - - void TerrainFeatureProcessor::OnTerrainMacroMaterialChanged(AZ::EntityId entityId, const MacroMaterialData& newMaterialData) - { - MacroMaterialData& data = FindOrCreateByEntityId(entityId, m_macroMaterials); - UpdateMacroMaterialData(data, newMaterialData); - } - - void TerrainFeatureProcessor::OnTerrainMacroMaterialRegionChanged( - AZ::EntityId entityId, [[maybe_unused]] const AZ::Aabb& oldRegion, const AZ::Aabb& newRegion) - { - MacroMaterialData& materialData = FindOrCreateByEntityId(entityId, m_macroMaterials); - for (SectorData& sectorData : m_sectorData) - { - bool overlapsOld = sectorData.m_aabb.Overlaps(materialData.m_bounds); - bool overlapsNew = sectorData.m_aabb.Overlaps(newRegion); - if (overlapsOld && !overlapsNew) - { - // Remove the macro material from this sector - for (uint16_t& idx : sectorData.m_macroMaterials) - { - if (m_macroMaterials.GetData(idx).m_entityId == entityId) - { - idx = sectorData.m_macroMaterials.back(); - sectorData.m_macroMaterials.pop_back(); - } - } - } - else if (overlapsNew && !overlapsOld) - { - // Add the macro material to this sector - if (sectorData.m_macroMaterials.size() < MaxMaterialsPerSector) - { - sectorData.m_macroMaterials.push_back(m_macroMaterials.GetIndexForData(&materialData)); - } - } - } - m_areaData.m_macroMaterialsUpdated = true; - materialData.m_bounds = newRegion; - } - - void TerrainFeatureProcessor::OnTerrainMacroMaterialDestroyed(AZ::EntityId entityId) - { - const MacroMaterialData* materialData = FindByEntityId(entityId, m_macroMaterials); - - if (materialData) - { - uint16_t destroyedMaterialIndex = m_macroMaterials.GetIndexForData(materialData); - ForOverlappingSectors(materialData->m_bounds, - [&](SectorData& sectorData) { - for (uint16_t& idx : sectorData.m_macroMaterials) - { - if (idx == destroyedMaterialIndex) - { - idx = sectorData.m_macroMaterials.back(); - sectorData.m_macroMaterials.pop_back(); - } - } - }); - } - - m_areaData.m_macroMaterialsUpdated = true; - RemoveByEntityId(entityId, m_macroMaterials); - } - - void TerrainFeatureProcessor::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(TerrainFPName, 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); + m_terrainBounds = worldBounds; + m_sampleSpacing = queryResolution; + m_heightmapNeedsUpdate = true; } void TerrainFeatureProcessor::OnRenderPipelinePassesChanged([[maybe_unused]] AZ::RPI::RenderPipeline* renderPipeline) @@ -371,551 +175,18 @@ namespace Terrain CacheForwardPass(); } - void TerrainFeatureProcessor::CheckDetailMaterialForDeletion(uint16_t detailMaterialId) + void TerrainFeatureProcessor::UpdateHeightmapImage() { - 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 != InvalidDetailImageIndex) - { - m_detailImageViews.at(imageIndex) = AZ::RPI::ImageSystemInterface::Get()->GetSystemImage(AZ::RPI::SystemImage::Magenta)->GetImageView(); - m_detailImageViewFreeList.push_back(imageIndex); - m_detailImagesNeedUpdate = true; - } - } - - m_detailMaterialShaderData.Release(bufferIndex); - m_detailMaterials.RemoveIndex(detailMaterialId); - } - } - - void TerrainFeatureProcessor::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(TerrainFPName, false, "Could not find surface tag to destroy for OnTerrainSurfaceMaterialMappingDestroyed()."); - } - - void TerrainFeatureProcessor::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 TerrainFeatureProcessor::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); - } - - uint16_t TerrainFeatureProcessor::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(m_detailMaterialShaderData.Reserve()); - UpdateDetailMaterialData(detailMaterialId, material); - } - return detailMaterialId; - } - - void TerrainFeatureProcessor::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(TerrainFPName, 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() expects the actaul type, not a reference type, so the reference needs to be removed. - using TypeRefRemoved = AZStd::remove_cvref_t; - ref = material->GetPropertyValue(index).GetValue(); - } - }; - - auto applyImage = [&](const char* const indexName, AZ::Data::Instance& 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(); - } - if (index.IsValid() && useTextureValue) - { - ref = material->GetPropertyValue(index).GetValue>(); - } - 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 == InvalidDetailImageIndex) - { - if (m_detailImageViewFreeList.size() > 0) - { - imageIndex = m_detailImageViewFreeList.back(); - m_detailImageViewFreeList.pop_back(); - } - else - { - imageIndex = aznumeric_cast(m_detailImageViews.size()); - m_detailImageViews.push_back(); - } - } - m_detailImageViews.at(imageIndex) = ref->GetImageView(); - m_detailImagesNeedUpdate = true; - } - else if (imageIndex != InvalidDetailImageIndex) - { - m_detailImageViews.at(imageIndex) = AZ::RPI::ImageSystemInterface::Get()->GetSystemImage(AZ::RPI::SystemImage::Magenta)->GetImageView(); - m_detailImageViewFreeList.push_back(imageIndex); - m_detailImagesNeedUpdate = true; - imageIndex = InvalidDetailImageIndex; - } - }; - - auto applyFlag = [&](const char* const indexName, DetailTextureFlags flagToSet) -> void - { - const auto index = getIndex(indexName); - if (index.IsValid()) - { - bool flagValue = material->GetPropertyValue(index).GetValue(); - 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(); - 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(); - 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_updateDetailMaterialBuffer = true; - } - - void TerrainFeatureProcessor::CheckUpdateDetailTexture(const Aabb2i& newBounds, const Vector2i& newCenter) - { - if (!m_detailTextureImage) - { - // If the m_detailTextureImage doesn't exist, create it and populate the entire texture - - const AZ::Data::Instance 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(TerrainFPName, 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; - 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; - } - UpdateDetailTexture(updateBounds, newBounds, newCenter); - } - - if (m_dirtyDetailRegion.IsValid()) - { - // 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(AZStd::roundf(clampedCoverage.GetMin().GetX() / DetailTextureScale)); - updateBounds.m_min.m_y = aznumeric_cast(AZStd::roundf(clampedCoverage.GetMin().GetY() / DetailTextureScale)); - updateBounds.m_max.m_x = aznumeric_cast(AZStd::roundf(clampedCoverage.GetMax().GetX() / DetailTextureScale)); - updateBounds.m_max.m_y = aznumeric_cast(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) - { - UpdateDetailTexture(updateBounds, newBounds, newCenter); - } - } - } - } - - } - - uint8_t TerrainFeatureProcessor::CalculateUpdateRegions(const Aabb2i& updateArea, const Aabb2i& textureBounds, const Vector2i& centerPixel, - AZStd::array& textureSpaceAreas, AZStd::array& 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; - } - - void TerrainFeatureProcessor::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 textureSpaceAreas; - AZStd::array scaledWorldSpaceAreas; - uint8_t updateAreaCount = CalculateUpdateRegions(updateArea, textureBounds, centerPixel, textureSpaceAreas, scaledWorldSpaceAreas); - - // 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 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(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(materialId); - float totalWeight = firstWeight + surfaceTagWeight.m_weight; - float blendWeight = 1.0f - (firstWeight / totalWeight); - pixels.at(index).m_blend = aznumeric_cast(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(left); - imageUpdateRequest.m_imageSubresourcePixelOffset.m_top = aznumeric_cast(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); - } - } - - uint16_t TerrainFeatureProcessor::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; - } - - void TerrainFeatureProcessor::UpdateTerrainData() - { - - const float queryResolution = m_areaData.m_sampleSpacing; - const AZ::Aabb& worldBounds = m_areaData.m_terrainBounds; - - int32_t heightmapImageXStart = aznumeric_cast(AZStd::ceilf(worldBounds.GetMin().GetX() / queryResolution)); - int32_t heightmapImageXEnd = aznumeric_cast(AZStd::floorf(worldBounds.GetMax().GetX() / queryResolution)) + 1; - int32_t heightmapImageYStart = aznumeric_cast(AZStd::ceilf(worldBounds.GetMin().GetY() / queryResolution)); - int32_t heightmapImageYEnd = aznumeric_cast(AZStd::floorf(worldBounds.GetMax().GetY() / queryResolution)) + 1; + int32_t heightmapImageXStart = aznumeric_cast(AZStd::ceilf(m_terrainBounds.GetMin().GetX() / m_sampleSpacing)); + int32_t heightmapImageXEnd = aznumeric_cast(AZStd::floorf(m_terrainBounds.GetMax().GetX() / m_sampleSpacing)) + 1; + int32_t heightmapImageYStart = aznumeric_cast(AZStd::ceilf(m_terrainBounds.GetMin().GetY() / m_sampleSpacing)); + int32_t heightmapImageYEnd = aznumeric_cast(AZStd::floorf(m_terrainBounds.GetMax().GetY() / m_sampleSpacing)) + 1; uint32_t heightmapImageWidth = heightmapImageXEnd - heightmapImageXStart; uint32_t heightmapImageHeight = heightmapImageYEnd - heightmapImageYStart; const AZ::RHI::Size heightmapSize = AZ::RHI::Size(heightmapImageWidth, heightmapImageHeight, 1); - if (!m_areaData.m_heightmapImage || m_areaData.m_heightmapImage->GetDescriptor().m_size != heightmapSize) + if (!m_heightmapImage || m_heightmapImage->GetDescriptor().m_size != heightmapSize) { const AZ::Data::Instance imagePool = AZ::RPI::ImageSystemInterface::Get()->GetSystemAttachmentPool(); AZ::RHI::ImageDescriptor imageDescriptor = AZ::RHI::ImageDescriptor::Create2D( @@ -923,24 +194,28 @@ namespace Terrain ); const AZ::Name TerrainHeightmapName = AZ::Name(TerrainHeightmapChars); - m_areaData.m_heightmapImage = AZ::RPI::AttachmentImage::Create(*imagePool.get(), imageDescriptor, TerrainHeightmapName, nullptr, nullptr); - AZ_Error(TerrainFPName, m_areaData.m_heightmapImage, "Failed to initialize the heightmap image."); + m_heightmapImage = AZ::RPI::AttachmentImage::Create(*imagePool.get(), imageDescriptor, TerrainHeightmapName, nullptr, nullptr); + AZ_Error(TerrainFPName, m_heightmapImage, "Failed to initialize the heightmap image."); // World size changed, so the whole height map needs updating. - m_dirtyRegion = worldBounds; - m_imagesNeedUpdate = true; + m_dirtyRegion = m_terrainBounds; + m_imageBindingsNeedUpdate = true; + } + + if (!m_dirtyRegion.IsValid()) + { + return; } - int32_t xStart = aznumeric_cast(AZStd::ceilf(m_dirtyRegion.GetMin().GetX() / queryResolution)); - int32_t xEnd = aznumeric_cast(AZStd::floorf(m_dirtyRegion.GetMax().GetX() / queryResolution)) + 1; - int32_t yStart = aznumeric_cast(AZStd::ceilf(m_dirtyRegion.GetMin().GetY() / queryResolution)); - int32_t yEnd = aznumeric_cast(AZStd::floorf(m_dirtyRegion.GetMax().GetY() / queryResolution)) + 1; + int32_t xStart = aznumeric_cast(AZStd::ceilf(m_dirtyRegion.GetMin().GetX() / m_sampleSpacing)); + int32_t xEnd = aznumeric_cast(AZStd::floorf(m_dirtyRegion.GetMax().GetX() / m_sampleSpacing)) + 1; + int32_t yStart = aznumeric_cast(AZStd::ceilf(m_dirtyRegion.GetMin().GetY() / m_sampleSpacing)); + int32_t yEnd = aznumeric_cast(AZStd::floorf(m_dirtyRegion.GetMax().GetY() / m_sampleSpacing)) + 1; uint32_t updateWidth = xEnd - xStart; uint32_t updateHeight = yEnd - yStart; AZStd::vector pixels; pixels.reserve(updateWidth * updateHeight); - { // Block other threads from accessing the surface data bus while we are in GetHeightFromFloats (which may call into the SurfaceData bus). // We lock our surface data mutex *before* checking / setting "isRequestInProgress" so that we prevent race conditions @@ -957,13 +232,13 @@ namespace Terrain { bool terrainExists = true; float terrainHeight = 0.0f; - float xPos = x * queryResolution; - float yPos = y * queryResolution; + float xPos = x * m_sampleSpacing; + float yPos = y * m_sampleSpacing; AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult( terrainHeight, &AzFramework::Terrain::TerrainDataRequests::GetHeightFromFloats, xPos, yPos, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists); - const float clampedHeight = AZ::GetClamp((terrainHeight - worldBounds.GetMin().GetZ()) / worldBounds.GetExtents().GetZ(), 0.0f, 1.0f); + const float clampedHeight = AZ::GetClamp((terrainHeight - m_terrainBounds.GetMin().GetZ()) / m_terrainBounds.GetExtents().GetZ(), 0.0f, 1.0f); const float expandedHeight = AZStd::roundf(clampedHeight * AZStd::numeric_limits::max()); const uint16_t uint16Height = aznumeric_cast(expandedHeight); @@ -972,11 +247,11 @@ namespace Terrain } } - if (m_areaData.m_heightmapImage) + if (m_heightmapImage) { constexpr uint32_t BytesPerPixel = sizeof(uint16_t); - const float left = xStart - (worldBounds.GetMin().GetX() / queryResolution); - const float top = yStart - (worldBounds.GetMin().GetY() / queryResolution); + const float left = xStart - (m_terrainBounds.GetMin().GetX() / m_sampleSpacing); + const float top = yStart - (m_terrainBounds.GetMin().GetY() / m_sampleSpacing); AZ::RHI::ImageUpdateRequest imageUpdateRequest; imageUpdateRequest.m_imageSubresourcePixelOffset.m_left = aznumeric_cast(left); @@ -984,41 +259,20 @@ namespace Terrain imageUpdateRequest.m_sourceSubresourceLayout.m_bytesPerRow = updateWidth * BytesPerPixel; imageUpdateRequest.m_sourceSubresourceLayout.m_bytesPerImage = updateWidth * updateHeight * BytesPerPixel; imageUpdateRequest.m_sourceSubresourceLayout.m_rowCount = updateHeight; - imageUpdateRequest.m_sourceSubresourceLayout.m_size.m_width = updateWidth; - imageUpdateRequest.m_sourceSubresourceLayout.m_size.m_height = updateHeight; - imageUpdateRequest.m_sourceSubresourceLayout.m_size.m_depth = 1; + imageUpdateRequest.m_sourceSubresourceLayout.m_size = AZ::RHI::Size(updateWidth, updateHeight, 1); imageUpdateRequest.m_sourceData = pixels.data(); - imageUpdateRequest.m_image = m_areaData.m_heightmapImage->GetRHIImage(); + imageUpdateRequest.m_image = m_heightmapImage->GetRHIImage(); - m_areaData.m_heightmapImage->UpdateImageContents(imageUpdateRequest); + m_heightmapImage->UpdateImageContents(imageUpdateRequest); } m_dirtyRegion = AZ::Aabb::CreateNull(); } void TerrainFeatureProcessor::PrepareMaterialData() - { - const auto layout = m_materialInstance->GetAsset()->GetObjectSrgLayout(); - - m_modelToWorldIndex = layout->FindShaderInputConstantIndex(AZ::Name(ShaderInputs::ModelToWorld)); - AZ_Error(TerrainFPName, m_modelToWorldIndex.IsValid(), "Failed to find shader input constant %s.", ShaderInputs::ModelToWorld); - - m_terrainDataIndex = layout->FindShaderInputConstantIndex(AZ::Name(ShaderInputs::TerrainData)); - AZ_Error(TerrainFPName, m_terrainDataIndex.IsValid(), "Failed to find shader input constant %s.", ShaderInputs::TerrainData); - - m_macroMaterialDataIndex = layout->FindShaderInputConstantIndex(AZ::Name(ShaderInputs::MacroMaterialData)); - AZ_Error(TerrainFPName, m_macroMaterialDataIndex.IsValid(), "Failed to find shader input constant %s.", ShaderInputs::MacroMaterialData); - - m_macroMaterialCountIndex = layout->FindShaderInputConstantIndex(AZ::Name(ShaderInputs::MacroMaterialCount)); - AZ_Error(TerrainFPName, m_macroMaterialCountIndex.IsValid(), "Failed to find shader input constant %s.", ShaderInputs::MacroMaterialCount); - - m_macroColorMapIndex = layout->FindShaderInputImageIndex(AZ::Name(ShaderInputs::MacroColorMap)); - AZ_Error(TerrainFPName, m_macroColorMapIndex.IsValid(), "Failed to find shader input constant %s.", ShaderInputs::MacroColorMap); - - m_macroNormalMapIndex = layout->FindShaderInputImageIndex(AZ::Name(ShaderInputs::MacroNormalMap)); - AZ_Error(TerrainFPName, m_macroNormalMapIndex.IsValid(), "Failed to find shader input constant %s.", ShaderInputs::MacroNormalMap); - + { m_terrainSrg = {}; + for (auto& shaderItem : m_materialInstance->GetShaderCollection()) { if (shaderItem.GetShaderAsset()->GetDrawListName() == AZ::Name("forward")) @@ -1034,76 +288,38 @@ namespace Terrain if (m_terrainSrg) { - const AZ::RHI::ShaderResourceGroupLayout* terrainSrgLayout = m_terrainSrg->GetLayout(); + if (m_imageArrayHandler->IsInitialized()) + { + m_imageArrayHandler->UpdateSrgIndices(m_terrainSrg, AZ::Name(TerrainSrgInputs::Textures)); + } + else + { + m_imageArrayHandler->Initialize(m_terrainSrg, AZ::Name(TerrainSrgInputs::Textures)); + } - m_detailMaterialIdPropertyIndex = terrainSrgLayout->FindShaderInputImageIndex(AZ::Name(TerrainSrgInputs::DetailMaterialIdImage)); - AZ_Error(TerrainFPName, m_detailMaterialIdPropertyIndex.IsValid(), "Failed to find view srg input constant %s.", TerrainSrgInputs::DetailMaterialIdImage); - - m_detailCenterPropertyIndex = terrainSrgLayout->FindShaderInputConstantIndex(AZ::Name(TerrainSrgInputs::DetailMaterialIdImageCenter)); - AZ_Error(TerrainFPName, m_detailCenterPropertyIndex.IsValid(), "Failed to find view srg input constant %s.", TerrainSrgInputs::DetailMaterialIdImageCenter); - - m_detailHalfPixelUvPropertyIndex = terrainSrgLayout->FindShaderInputConstantIndex(AZ::Name(TerrainSrgInputs::DetailHalfPixelUv)); - AZ_Error(TerrainFPName, m_detailHalfPixelUvPropertyIndex.IsValid(), "Failed to find view srg input constant %s.", TerrainSrgInputs::DetailHalfPixelUv); - - m_detailAabbPropertyIndex = terrainSrgLayout->FindShaderInputConstantIndex(AZ::Name(TerrainSrgInputs::DetailAabb)); - AZ_Error(TerrainFPName, m_detailAabbPropertyIndex.IsValid(), "Failed to find view srg input constant %s.", TerrainSrgInputs::DetailAabb); - - m_detailTexturesIndex = terrainSrgLayout->FindShaderInputImageUnboundedArrayIndex(AZ::Name(TerrainSrgInputs::DetailTextures)); - AZ_Error(TerrainFPName, m_detailTexturesIndex.IsValid(), "Failed to find view srg input constant %s.", TerrainSrgInputs::DetailTextures); - - // 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); + if (m_macroMaterialManager.IsInitialized()) + { + m_macroMaterialManager.UpdateSrgIndices(m_terrainSrg); + } + else + { + m_macroMaterialManager.Initialize(m_imageArrayHandler, m_terrainSrg); + } + + if (m_detailMaterialManager.IsInitialized()) + { + m_detailMaterialManager.UpdateSrgIndices(m_terrainSrg); + } + else + { + m_detailMaterialManager.Initialize(m_imageArrayHandler, m_terrainSrg); + } } - - // Find any macro materials that have already been created. - TerrainMacroMaterialRequestBus::EnumerateHandlers( - [&](TerrainMacroMaterialRequests* handler) - { - MacroMaterialData macroMaterial = handler->GetTerrainMacroMaterialData(); - AZ::EntityId entityId = *(Terrain::TerrainMacroMaterialRequestBus::GetCurrentBusId()); - OnTerrainMacroMaterialCreated(entityId, macroMaterial); - return true; - } - ); - TerrainMacroMaterialNotificationBus::Handler::BusConnect(); - - // Find any detail material areas that have already been created. - TerrainAreaMaterialRequestBus::EnumerateHandlers( - [&](TerrainAreaMaterialRequests* handler) - { - const AZ::Aabb& bounds = handler->GetTerrainSurfaceMaterialRegion(); - const AZStd::vector 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(); - - } - - void TerrainFeatureProcessor::UpdateMacroMaterialData(MacroMaterialData& macroMaterialData, const MacroMaterialData& newMaterialData) - { - macroMaterialData = newMaterialData; - - if (macroMaterialData.m_bounds.IsValid()) + else { - m_areaData.m_macroMaterialsUpdated = true; + m_imageArrayHandler->Reset(); + m_macroMaterialManager.Reset(); + m_detailMaterialManager.Reset(); } } @@ -1111,111 +327,13 @@ namespace Terrain { AZ_PROFILE_FUNCTION(AzRender); - const AZ::Aabb& terrainBounds = m_areaData.m_terrainBounds; - - if (!terrainBounds.IsValid()) + if (!m_terrainBounds.IsValid()) { return; } if (m_materialInstance && m_materialInstance->CanCompile()) { - if (m_areaData.m_rebuildSectors) - { - // Something about the whole world changed, so the sectors need to be rebuilt - - m_areaData.m_rebuildSectors = false; - - m_sectorData.clear(); - const float xFirstPatchStart = AZStd::floorf(terrainBounds.GetMin().GetX() / GridMeters) * GridMeters; - const float xLastPatchStart = AZStd::floorf(terrainBounds.GetMax().GetX() / GridMeters) * GridMeters; - const float yFirstPatchStart = AZStd::floorf(terrainBounds.GetMin().GetY() / GridMeters) * GridMeters; - const float yLastPatchStart = AZStd::floorf(terrainBounds.GetMax().GetY() / GridMeters) * GridMeters; - - const auto& materialAsset = m_materialInstance->GetAsset(); - const auto& shaderAsset = materialAsset->GetMaterialTypeAsset()->GetShaderAssetForObjectSrg(); - - for (float yPatch = yFirstPatchStart; yPatch <= yLastPatchStart; yPatch += GridMeters) - { - for (float xPatch = xFirstPatchStart; xPatch <= xLastPatchStart; xPatch += GridMeters) - { - auto objectSrg = AZ::RPI::ShaderResourceGroup::Create(shaderAsset, materialAsset->GetObjectSrgLayout()->GetName()); - if (!objectSrg) - { - AZ_Warning(TerrainFPName, false, "Failed to create a new shader resource group, skipping."); - continue; - } - - m_sectorData.push_back(); - SectorData& sectorData = m_sectorData.back(); - - for (auto& lod : m_patchModel->GetLods()) - { - AZ::RPI::ModelLod& modelLod = *lod.get(); - sectorData.m_drawPackets.emplace_back(modelLod, 0, m_materialInstance, objectSrg); - AZ::RPI::MeshDrawPacket& drawPacket = sectorData.m_drawPackets.back(); - - // 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(TerrainFPName, 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(*GetParentScene(), true); - } - - sectorData.m_aabb = - AZ::Aabb::CreateFromMinMax( - AZ::Vector3(xPatch, yPatch, terrainBounds.GetMin().GetZ()), - AZ::Vector3(xPatch + GridMeters, yPatch + GridMeters, terrainBounds.GetMax().GetZ()) - ); - sectorData.m_srg = objectSrg; - } - } - - if (m_areaData.m_macroMaterialsUpdated) - { - // sectors were rebuilt, so any cached macro material data needs to be regenerated - for (SectorData& sectorData : m_sectorData) - { - for (MacroMaterialData& macroMaterialData : m_macroMaterials.GetDataVector()) - { - if (macroMaterialData.m_bounds.Overlaps(sectorData.m_aabb)) - { - sectorData.m_macroMaterials.push_back(m_macroMaterials.GetIndexForData(¯oMaterialData)); - if (sectorData.m_macroMaterials.size() == MaxMaterialsPerSector) - { - break; - } - } - } - } - } - } - else if (m_forceRebuildDrawPackets) - { - for (auto& sectorData : m_sectorData) - { - for (auto& drawPacket : sectorData.m_drawPackets) - { - drawPacket.Update(*GetParentScene(), true); - } - } - } - m_forceRebuildDrawPackets = false; - - if (m_areaData.m_heightmapUpdated) - { - UpdateTerrainData(); - } - - if (m_updateDetailMaterialBuffer) - { - m_updateDetailMaterialBuffer = false; - m_detailMaterialDataBuffer.UpdateBuffer(m_detailMaterialShaderData.GetRawData(), aznumeric_cast(m_detailMaterialShaderData.GetSize())); - } - AZ::Vector3 cameraPosition = AZ::Vector3::CreateZero(); for (auto& view : process.m_views) { @@ -1226,198 +344,59 @@ namespace Terrain } } - if (m_dirtyDetailRegion.IsValid() || !cameraPosition.IsClose(m_previousCameraPosition) || m_detailImagesNeedUpdate) + if (m_meshManager.IsInitialized()) { - int32_t newDetailTexturePosX = aznumeric_cast(AZStd::roundf(cameraPosition.GetX() / DetailTextureScale)); - int32_t newDetailTexturePosY = aznumeric_cast(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); - - if (m_terrainSrg) - { - m_terrainSrg->SetConstant(m_detailAabbPropertyIndex, detailAabb); - m_terrainSrg->SetConstant(m_detailHalfPixelUvPropertyIndex, 0.5f / DetailTextureSize); - m_terrainSrg->SetConstant(m_detailCenterPropertyIndex, detailUvOffset); - - m_detailMaterialDataBuffer.UpdateSrg(m_terrainSrg.get()); + bool surfacesRebuilt = false; + surfacesRebuilt = m_meshManager.CheckRebuildSurfaces(m_materialInstance, *GetParentScene()); + if (m_forceRebuildDrawPackets && !surfacesRebuilt) + { + m_meshManager.RebuildDrawPackets(*GetParentScene()); } + m_forceRebuildDrawPackets = false; } - if (m_areaData.m_heightmapUpdated || m_areaData.m_macroMaterialsUpdated) - { - // Currently when anything in the heightmap changes we're updating all the srgs, but this could probably - // be optimized to only update the srgs that changed. - - m_areaData.m_heightmapUpdated = false; - m_areaData.m_macroMaterialsUpdated = false; - - AZStd::array uvStep = - { - 1.0f / aznumeric_cast(m_areaData.m_terrainBounds.GetXExtent() / m_areaData.m_sampleSpacing), - 1.0f / aznumeric_cast(m_areaData.m_terrainBounds.GetYExtent() / m_areaData.m_sampleSpacing), - }; - - for (SectorData& sectorData : m_sectorData) - { - ShaderTerrainData terrainDataForSrg; - - const float xPatch = sectorData.m_aabb.GetMin().GetX(); - const float yPatch = sectorData.m_aabb.GetMin().GetY(); - - terrainDataForSrg.m_uvMin = { - (xPatch - terrainBounds.GetMin().GetX()) / terrainBounds.GetXExtent(), - (yPatch - terrainBounds.GetMin().GetY()) / terrainBounds.GetYExtent() - }; - - terrainDataForSrg.m_uvMax = { - ((xPatch + GridMeters) - terrainBounds.GetMin().GetX()) / terrainBounds.GetXExtent(), - ((yPatch + GridMeters) - terrainBounds.GetMin().GetY()) / terrainBounds.GetYExtent() - }; - - terrainDataForSrg.m_uvStep = uvStep; - - AZ::Transform transform = m_areaData.m_transform; - transform.SetTranslation(xPatch, yPatch, m_areaData.m_transform.GetTranslation().GetZ()); - - terrainDataForSrg.m_sampleSpacing = m_areaData.m_sampleSpacing; - terrainDataForSrg.m_heightScale = terrainBounds.GetZExtent(); - - sectorData.m_srg->SetConstant(m_terrainDataIndex, terrainDataForSrg); - - AZStd::array macroMaterialData; - - uint32_t i = 0; - for (; i < sectorData.m_macroMaterials.size(); ++i) - { - const MacroMaterialData& materialData = m_macroMaterials.GetData(sectorData.m_macroMaterials.at(i)); - ShaderMacroMaterialData& shaderData = macroMaterialData.at(i); - const AZ::Aabb& materialBounds = materialData.m_bounds; - - // Use reverse coordinates (1 - y) for the y direction so that the lower left corner of the macro material images - // map to the lower left corner in world space. This will match up with the height uv coordinate mapping. - shaderData.m_uvMin = { - (xPatch - materialBounds.GetMin().GetX()) / materialBounds.GetXExtent(), - 1.0f - ((yPatch - materialBounds.GetMin().GetY()) / materialBounds.GetYExtent()) - }; - shaderData.m_uvMax = { - ((xPatch + GridMeters) - materialBounds.GetMin().GetX()) / materialBounds.GetXExtent(), - 1.0f - (((yPatch + GridMeters) - materialBounds.GetMin().GetY()) / materialBounds.GetYExtent()) - }; - shaderData.m_normalFactor = materialData.m_normalFactor; - shaderData.m_flipNormalX = materialData.m_normalFlipX; - shaderData.m_flipNormalY = materialData.m_normalFlipY; - - const AZ::RHI::ImageView* colorImageView = materialData.m_colorImage ? materialData.m_colorImage->GetImageView() : nullptr; - sectorData.m_srg->SetImageView(m_macroColorMapIndex, colorImageView, i); - - const AZ::RHI::ImageView* normalImageView = materialData.m_normalImage ? materialData.m_normalImage->GetImageView() : nullptr; - sectorData.m_srg->SetImageView(m_macroNormalMapIndex, normalImageView, i); - - // set flags for which images are used. - shaderData.m_mapsInUse = (colorImageView ? ColorImageUsed : 0) | (normalImageView ? NormalImageUsed : 0); - } - for (; i < sectorData.m_macroMaterials.capacity(); ++i) - { - sectorData.m_srg->SetImageView(m_macroColorMapIndex, nullptr, i); - sectorData.m_srg->SetImageView(m_macroNormalMapIndex, nullptr, i); - } - - sectorData.m_srg->SetConstantArray(m_macroMaterialDataIndex, macroMaterialData); - sectorData.m_srg->SetConstant(m_macroMaterialCountIndex, aznumeric_cast(sectorData.m_macroMaterials.size())); - - const AZ::Matrix3x4 matrix3x4 = AZ::Matrix3x4::CreateFromTransform(transform); - sectorData.m_srg->SetConstant(m_modelToWorldIndex, matrix3x4); - - sectorData.m_srg->Compile(); - } - } - - // Currently there seems to be a bug in unbounded image arrays where flickering can occur if this isn't updated every frame. - if (m_terrainSrg/* && m_detailImagesUpdated*/) - { - AZStd::array_view imageViews(m_detailImageViews.data(), m_detailImageViews.size()); - [[maybe_unused]] bool result = m_terrainSrg->SetImageViewUnboundedArray(m_detailTexturesIndex, imageViews); - AZ_Error(TerrainFPName, result, "Failed to set image view unbounded array into shader resource group."); - m_detailImagesNeedUpdate = false; - } - } - - 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(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(sectorData.m_drawPackets.size() - 1)); - view->AddDrawPacket(sectorData.m_drawPackets.at(lodToRender).GetRHIDrawPacket()); - } - } - } - - if (m_detailTextureImage && m_areaData.m_heightmapImage && m_imagesNeedUpdate) - { - m_imagesNeedUpdate = false; - for (auto& view : process.m_views) - { - auto viewSrg = view->GetShaderResourceGroup(); - viewSrg->SetImage(m_heightmapPropertyIndex, m_areaData.m_heightmapImage); - } if (m_terrainSrg) { - m_terrainSrg->SetImage(m_detailMaterialIdPropertyIndex, m_detailTextureImage); + if (m_macroMaterialManager.IsInitialized()) + { + m_macroMaterialManager.Update(m_terrainSrg); + } + + if (m_detailMaterialManager.IsInitialized()) + { + m_detailMaterialManager.Update(cameraPosition, m_terrainSrg); + } } + + if (m_heightmapNeedsUpdate) + { + UpdateHeightmapImage(); + m_heightmapNeedsUpdate = false; + } + + if (m_imageArrayHandler->IsInitialized()) + { + bool result [[maybe_unused]] = m_imageArrayHandler->UpdateSrg(m_terrainSrg); + AZ_Error(TerrainFPName, result, "Failed to set image view unbounded array into shader resource group."); + } + } + + if (m_meshManager.IsInitialized()) + { + m_meshManager.DrawMeshes(process); + } + + if (m_heightmapImage && m_imageBindingsNeedUpdate) + { + WorldShaderData worldData; + m_terrainBounds.GetMin().StoreToFloat3(worldData.m_min.data()); + m_terrainBounds.GetMax().StoreToFloat3(worldData.m_max.data()); + + m_imageBindingsNeedUpdate = false; + + auto sceneSrg = GetParentScene()->GetShaderResourceGroup(); + sceneSrg->SetImage(m_heightmapPropertyIndex, m_heightmapImage); + sceneSrg->SetConstant(m_worldDataIndex, worldData); } if (m_materialInstance) @@ -1432,142 +411,11 @@ namespace Terrain } } - void TerrainFeatureProcessor::InitializeTerrainPatch(uint16_t gridSize, float gridSpacing, PatchData& patchdata) - { - patchdata.m_positions.clear(); - patchdata.m_uvs.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); - patchdata.m_uvs.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(x) * gridSpacing, aznumeric_cast(y) * gridSpacing }); - patchdata.m_uvs.push_back({ aznumeric_cast(x) / gridSize, aznumeric_cast(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> TerrainFeatureProcessor::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(bufferViewDescriptor.m_elementSize) * static_cast(bufferViewDescriptor.m_elementCount); - - creator.SetBuffer(data, bufferDescriptor.m_byteCount, bufferDescriptor); - creator.SetBufferViewDescriptor(bufferViewDescriptor); - creator.SetUseCommonPool(AZ::RPI::CommonBufferPoolType::StaticInputAssembly); - - AZ::Data::Asset bufferAsset; - if (creator.End(bufferAsset)) - { - bufferAsset.SetHint(bufferName); - return AZ::Success(bufferAsset); - } - - return AZ::Failure(); - } - - bool TerrainFeatureProcessor::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, gridSpacing, patchData); - - const auto positionBufferViewDesc = AZ::RHI::BufferViewDescriptor::CreateTyped(0, aznumeric_cast(patchData.m_positions.size()), AZ::RHI::Format::R32G32_FLOAT); - const auto positionsOutcome = CreateBufferAsset(patchData.m_positions.data(), positionBufferViewDesc, "TerrainPatchPositions"); - - const auto uvBufferViewDesc = AZ::RHI::BufferViewDescriptor::CreateTyped(0, aznumeric_cast(patchData.m_uvs.size()), AZ::RHI::Format::R32G32_FLOAT); - const auto uvsOutcome = CreateBufferAsset(patchData.m_uvs.data(), uvBufferViewDesc, "TerrainPatchUvs"); - - const auto indexBufferViewDesc = AZ::RHI::BufferViewDescriptor::CreateTyped(0, aznumeric_cast(patchData.m_indices.size()), AZ::RHI::Format::R16_UINT); - const auto indicesOutcome = CreateBufferAsset(patchData.m_indices.data(), indexBufferViewDesc, "TerrainPatchIndices"); - - if (!positionsOutcome.IsSuccess() || !uvsOutcome.IsSuccess() || !indicesOutcome.IsSuccess()) - { - AZ_Error(TerrainFPName, 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.AddMeshStreamBuffer(AZ::RHI::ShaderSemantic{ "UV" }, AZ::Name(), {uvsOutcome.GetValue(), uvBufferViewDesc}); - 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 modelLodAsset; - modelLodAssetCreator.End(modelLodAsset); - - modelAssetCreator.AddLodAsset(AZStd::move(modelLodAsset)); - - gridSize = gridSize / 2; - gridSpacing *= 2.0f; - } - - AZ::Data::Asset modelAsset; - bool success = modelAssetCreator.End(modelAsset); - - m_patchModel = AZ::RPI::Model::FindOrCreate(modelAsset); - - return success; - } - void TerrainFeatureProcessor::OnMaterialReinitialized([[maybe_unused]] const MaterialInstance& material) { PrepareMaterialData(); - for (auto& sectorData : m_sectorData) - { - for (auto& drawPacket : sectorData.m_drawPackets) - { - drawPacket.Update(*GetParentScene()); - } - } - m_imagesNeedUpdate = true; - m_detailImagesNeedUpdate = true; + m_forceRebuildDrawPackets = true; + m_imageBindingsNeedUpdate = true; } void TerrainFeatureProcessor::SetWorldSize([[maybe_unused]] AZ::Vector2 sizeInMeters) @@ -1576,62 +424,6 @@ namespace Terrain // larger but this will limit how much is rendered. } - template - T* TerrainFeatureProcessor::FindByEntityId(AZ::EntityId entityId, AZ::Render::IndexedDataVector& container) - { - for (T& data : container.GetDataVector()) - { - if (data.m_entityId == entityId) - { - return &data; - } - } - return nullptr; - } - - template - T& TerrainFeatureProcessor::FindOrCreateByEntityId(AZ::EntityId entityId, AZ::Render::IndexedDataVector& container) - { - T* dataPtr = FindByEntityId(entityId, container); - if (dataPtr != nullptr) - { - return *dataPtr; - } - - const uint16_t slotId = container.GetFreeSlotIndex(); - AZ_Assert(slotId != AZ::Render::IndexedDataVector::NoFreeSlot, "Ran out of indices"); - - T& data = container.GetData(slotId); - data.m_entityId = entityId; - return data; - } - - template - void TerrainFeatureProcessor::RemoveByEntityId(AZ::EntityId entityId, AZ::Render::IndexedDataVector& container) - { - for (T& data : container.GetDataVector()) - { - if (data.m_entityId == entityId) - { - container.RemoveData(&data); - return; - } - } - AZ_Assert(false, "Entity Id not found in container.") - } - - template - void TerrainFeatureProcessor::ForOverlappingSectors(const AZ::Aabb& bounds, Callback callback) - { - for (SectorData& sectorData : m_sectorData) - { - if (sectorData.m_aabb.Overlaps(bounds)) - { - callback(sectorData); - } - } - } - void TerrainFeatureProcessor::CacheForwardPass() { auto rasterPassFilter = AZ::RPI::PassFilter::CreateWithPassClass(); @@ -1653,59 +445,5 @@ namespace Terrain ); } - auto TerrainFeatureProcessor::Vector2i::operator+(const Vector2i& rhs) const -> Vector2i - { - Vector2i offsetPoint = *this; - offsetPoint += rhs; - return offsetPoint; - } - - auto TerrainFeatureProcessor::Vector2i::operator+=(const Vector2i& rhs) -> Vector2i& - { - m_x += rhs.m_x; - m_y += rhs.m_y; - return *this; - } - - auto TerrainFeatureProcessor::Vector2i::operator-(const Vector2i& rhs) const -> Vector2i - { - return *this + -rhs; - } - - auto TerrainFeatureProcessor::Vector2i::operator-=(const Vector2i& rhs) -> Vector2i& - { - return *this += -rhs; - } - - auto TerrainFeatureProcessor::Vector2i::operator-() const -> Vector2i - { - return {-m_x, -m_y}; - } - - auto TerrainFeatureProcessor::Aabb2i::operator+(const Vector2i& rhs) const -> Aabb2i - { - return { m_min + rhs, m_max + rhs }; - } - - auto TerrainFeatureProcessor::Aabb2i::operator-(const Vector2i& rhs) const -> Aabb2i - { - return *this + -rhs; - } - - auto TerrainFeatureProcessor::Aabb2i::GetClamped(Aabb2i rhs) const -> Aabb2i - { - 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 TerrainFeatureProcessor::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; - } } diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.h b/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.h index 7d68e6b185..016c0d478a 100644 --- a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.h +++ b/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.h @@ -8,20 +8,17 @@ #pragma once -#include - #include -#include -#include + +#include +#include +#include +#include #include #include -#include #include #include -#include -#include -#include 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: - + + static constexpr auto InvalidImageIndex = AZ::Render::BindlessImageArrayHandler::InvalidImageIndex; using MaterialInstance = AZ::Data::Instance; - static constexpr uint32_t MaxMaterialsPerSector = 4; - - enum MacroMaterialFlags - { - ColorImageUsed = 0b01, - NormalImageUsed = 0b10, - }; - - struct ShaderTerrainData // Must align with struct in Object Srg - { - AZStd::array m_uvMin{ 0.0f, 0.0f }; - AZStd::array m_uvMax{ 1.0f, 1.0f }; - AZStd::array 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 m_uvMin{ 0.0f, 0.0f }; - AZStd::array 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 m_positions; - AZStd::vector m_uvs; - AZStd::vector m_indices; - }; - struct SectorData + struct WorldShaderData { - AZ::Data::Instance m_srg; // Hold on to ref so it's not dropped - AZ::Aabb m_aabb; - AZStd::fixed_vector m_drawPackets; - AZStd::fixed_vector 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 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 m_colorImage; - AZ::Data::Instance m_normalImage; - AZ::Data::Instance m_roughnessImage; - AZ::Data::Instance m_metalnessImage; - AZ::Data::Instance m_specularF0Image; - AZ::Data::Instance m_occlusionImage; - AZ::Data::Instance 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 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; - }; - - struct DetailTextureLocation - { - uint16_t m_index; - AZ::Data::Instance m_image; + AZStd::array m_min{ 0.0f, 0.0f, 0.0f }; + float padding1{ 0.0f }; + AZStd::array 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& textureSpaceAreas, AZStd::array& scaledWorldSpaceAreas); + void TerrainHeightOrSettingsUpdated(const AZ::Aabb& dirtyRegion); void ProcessSurfaces(const FeatureProcessor::RenderPacket& process); - template - T* FindByEntityId(AZ::EntityId entityId, AZ::Render::IndexedDataVector& container); - template - T& FindOrCreateByEntityId(AZ::EntityId entityId, AZ::Render::IndexedDataVector& container); - template - void RemoveByEntityId(AZ::EntityId entityId, AZ::Render::IndexedDataVector& container); - - template - void ForOverlappingSectors(const AZ::Aabb& bounds, Callback callback); - - AZ::Outcome> 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 m_imageArrayHandler; AZStd::unique_ptr m_materialAssetLoader; MaterialInstance m_materialInstance; + AZ::Data::Instance m_terrainSrg; + AZ::Data::Instance 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 m_patchModel; - AZ::Vector3 m_previousCameraPosition = AZ::Vector3(AZStd::numeric_limits::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 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 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 m_sectorData; + AZ::RPI::ShaderSystemInterface::GlobalShaderOptionUpdatedEvent::Handler m_handleGlobalShaderOptionUpdate; - AZ::Render::IndexedDataVector m_macroMaterials; - AZ::Render::IndexedDataVector m_detailMaterials; - AZ::Render::IndexedDataVector m_detailMaterialRegions; - AZ::Render::SparseVector m_detailMaterialShaderData; - AZ::Render::GpuBufferHandler m_detailMaterialDataBuffer; AZ::RPI::RenderPass* m_forwardPass; - - AZStd::vector m_detailImageViews; - AZStd::vector m_detailImageViewFreeList; - bool m_detailImagesNeedUpdate{ false }; }; } diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainMacroMaterialBus.cpp b/Gems/Terrain/Code/Source/TerrainRenderer/TerrainMacroMaterialBus.cpp index b1c6f2d54b..7f84874e63 100644 --- a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainMacroMaterialBus.cpp +++ b/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(context)) + { + behaviorContext->Class() + ->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(context)) { behaviorContext->EBus("TerrainMacroMaterialRequestBus") diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainMacroMaterialBus.h b/Gems/Terrain/Code/Source/TerrainRenderer/TerrainMacroMaterialBus.h index 9a7151da56..abcf4d4df0 100644 --- a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainMacroMaterialBus.h +++ b/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(); diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainMacroMaterialManager.cpp b/Gems/Terrain/Code/Source/TerrainRenderer/TerrainMacroMaterialManager.cpp new file mode 100644 index 0000000000..82349cdcdb --- /dev/null +++ b/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 + +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& bindlessImageHandler, + AZ::Data::Instance& 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& 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& 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 + 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(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(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& 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(m_terrainBounds.GetXExtent() / MacroMaterialGridSize) + 1; + m_tilesY = aznumeric_cast(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(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); + } + } + +} diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainMacroMaterialManager.h b/Gems/Terrain/Code/Source/TerrainRenderer/TerrainMacroMaterialManager.h new file mode 100644 index 0000000000..6a603eeced --- /dev/null +++ b/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 +#include +#include +#include +#include + +namespace Terrain +{ + class TerrainMacroMaterialManager + : private TerrainMacroMaterialNotificationBus::Handler + , private AzFramework::Terrain::TerrainDataNotificationBus::Handler + { + public: + + TerrainMacroMaterialManager() = default; + ~TerrainMacroMaterialManager() = default; + + void Initialize( + const AZStd::shared_ptr& bindlessImageHandler, + AZ::Data::Instance& terrainSrg); + void Reset(); + bool IsInitialized(); + bool UpdateSrgIndices(AZ::Data::Instance& terrainSrg); + + void Update(AZ::Data::Instance& 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 m_boundsMin{ 0.0f, 0.0f }; + AZStd::array 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 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 + 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 m_macroMaterialShaderData; + AZStd::vector m_macroMaterialEntities; // Same as above, but used to track entity ids which aren't needed by the shader. + AZStd::map 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 m_bindlessImageHandler; + AZ::Render::GpuBufferHandler m_macroMaterialDataBuffer; + + AZ::RHI::ShaderInputConstantIndex m_macroMaterialGridIndex; + + bool m_terrainSizeChanged{ false }; + bool m_bufferNeedsUpdate{ false }; + bool m_isInitialized{ false }; + + }; +} diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainMeshManager.cpp b/Gems/Terrain/Code/Source/TerrainRenderer/TerrainMeshManager.cpp new file mode 100644 index 0000000000..d689d2635c --- /dev/null +++ b/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 + +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +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(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(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(x) / gridSize, aznumeric_cast(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> 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(bufferViewDescriptor.m_elementSize) * static_cast(bufferViewDescriptor.m_elementCount); + + creator.SetBuffer(data, bufferDescriptor.m_byteCount, bufferDescriptor); + creator.SetBufferViewDescriptor(bufferViewDescriptor); + creator.SetUseCommonPool(AZ::RPI::CommonBufferPoolType::StaticInputAssembly); + + AZ::Data::Asset 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(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(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 modelLodAsset; + modelLodAssetCreator.End(modelLodAsset); + + modelAssetCreator.AddLodAsset(AZStd::move(modelLodAsset)); + + gridSize = gridSize / 2; + gridSpacing *= 2.0f; + } + + AZ::Data::Asset modelAsset; + bool success = modelAssetCreator.End(modelAsset); + + m_patchModel = AZ::RPI::Model::FindOrCreate(modelAsset); + + return success; + } + + template + void TerrainMeshManager::ForOverlappingSectors(const AZ::Aabb& bounds, Callback callback) + { + for (SectorData& sectorData : m_sectorData) + { + if (sectorData.m_aabb.Overlaps(bounds)) + { + callback(sectorData); + } + } + } + +} diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainMeshManager.h b/Gems/Terrain/Code/Source/TerrainRenderer/TerrainMeshManager.h new file mode 100644 index 0000000000..7252a85686 --- /dev/null +++ b/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 +#include +#include +#include + +#include + +#include + +#include +#include +#include + +#include + + +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; + + 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 m_positions; + AZStd::vector m_indices; + }; + + struct SectorData + { + AZStd::fixed_vector m_drawPackets; + AZStd::fixed_vector, 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 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> CreateBufferAsset( + const void* data, const AZ::RHI::BufferViewDescriptor& bufferViewDescriptor, const AZStd::string& bufferName); + + void InitializeTerrainPatch(uint16_t gridSize, PatchData& patchdata); + bool InitializePatchModel(); + + template + void ForOverlappingSectors(const AZ::Aabb& bounds, Callback callback); + + AZStd::vector m_sectorData; + AZ::Data::Instance m_patchModel; + + AZ::Aabb m_worldBounds{ AZ::Aabb::CreateNull() }; + float m_sampleSpacing = 1.0f; + + bool m_isInitialized{ false }; + bool m_rebuildSectors{ true }; + + }; +} diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/Vector2i.cpp b/Gems/Terrain/Code/Source/TerrainRenderer/Vector2i.cpp new file mode 100644 index 0000000000..1df945b88c --- /dev/null +++ b/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 + +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}; + } +} diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/Vector2i.h b/Gems/Terrain/Code/Source/TerrainRenderer/Vector2i.h new file mode 100644 index 0000000000..1244972fe9 --- /dev/null +++ b/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 + +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 }; + + }; +} diff --git a/Gems/Terrain/Code/terrain_files.cmake b/Gems/Terrain/Code/terrain_files.cmake index a4f39056e4..ab19d33618 100644 --- a/Gems/Terrain/Code/terrain_files.cmake +++ b/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