From 97920feaf16ec8dfa3d2cd911555a914c67b0c80 Mon Sep 17 00:00:00 2001 From: Ken Pruiksma Date: Wed, 27 Oct 2021 10:12:13 -0500 Subject: [PATCH] Detail material Id texture created from surface weights. (#4984) * Added some structs for detail materials Signed-off-by: Ken Pruiksma * Added some template functions for looking up materials. Added lookups for all the relevant detail material fields in StandardPBR. Signed-off-by: Ken Pruiksma * Added some structs for detail materials Signed-off-by: Ken Pruiksma * Added some template functions for looking up materials. Added lookups for all the relevant detail material fields in StandardPBR. Signed-off-by: Ken Pruiksma * Added support for generating a detail material texture with IDs populated from surface weights. Signed-off-by: Ken Pruiksma * Updated TerrainAreaMaterailRequestBus to have separate calls for region vs materials instead of the awkward out parameter Update MaterialPropertyDescriptor so that you can retrieve enum names by ID Several bug fixes / updates to the terrain feature processor dealing with detail materials. Signed-off-by: Ken Pruiksma * Updating detail material texture based on offsets. Not quite working yet but close. Added visualization for detail material in shader (currently on, will be turned off before final commit) Signed-off-by: Ken Pruiksma * Small bugfixes * Fix compile error in non-unity builds * Fixed backwards x/y loops causing the wrong pixels to update * Fixed selection of surface type with multiple surface weights Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Adding seam to detail texture debug display. Offseting edges by a half-pixel to avoid bleed. Disabling debugging detail textures by default. Signed-off-by: Ken Pruiksma * Missing file from last commit for detail material change. Signed-off-by: Ken Pruiksma * Cleanups Signed-off-by: Ken Pruiksma * bug fix Signed-off-by: Ken Pruiksma * Bug fix in the terrain fp for TerrainAreaMaterialRequestBus returning incomplete materials on GetSurfaceMaterialMappings Signed-off-by: Ken Pruiksma * Some PR updates. Exposing detail material id debugging through a cvar. Signed-off-by: Ken Pruiksma * Various updates from review. Signed-off-by: Ken Pruiksma * PR updates dealing with debug texture boundary line. Signed-off-by: Ken Pruiksma * Hiding some fields from the terrain material Signed-off-by: Ken Pruiksma * Fixing type in generic lambda for linux / android Signed-off-by: Ken Pruiksma Co-authored-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> --- .../Material/MaterialPropertyDescriptor.h | 3 + .../Material/MaterialPropertyDescriptor.cpp | 10 + .../Materials/Terrain/PbrTerrain.materialtype | 47 +- .../Shaders/Terrain/TerrainCommon.azsli | 14 + .../Terrain/TerrainPBR_ForwardPass.azsl | 45 ++ .../TerrainSurfaceMaterialsListComponent.cpp | 19 +- .../TerrainSurfaceMaterialsListComponent.h | 5 +- .../TerrainAreaMaterialRequestBus.h | 9 +- .../TerrainFeatureProcessor.cpp | 734 +++++++++++++++++- .../TerrainRenderer/TerrainFeatureProcessor.h | 155 +++- 10 files changed, 992 insertions(+), 49 deletions(-) diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialPropertyDescriptor.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialPropertyDescriptor.h index 085256f6d8..76bb2a6113 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialPropertyDescriptor.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialPropertyDescriptor.h @@ -106,6 +106,9 @@ namespace AZ static constexpr uint32_t InvalidEnumValue = std::numeric_limits::max(); uint32_t GetEnumValue(const AZ::Name& enumName) const; + //! Returns the name of the enum from its index. An empty name is returned for an invalid id. + const AZ::Name& GetEnumName(uint32_t enumValue) const; + //! Returns the unique name ID of this property const Name& GetName() const; diff --git a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialPropertyDescriptor.cpp b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialPropertyDescriptor.cpp index 700ac41003..5d0a88a6d3 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialPropertyDescriptor.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialPropertyDescriptor.cpp @@ -203,6 +203,16 @@ namespace AZ return InvalidEnumValue; } + + const AZ::Name& MaterialPropertyDescriptor::GetEnumName(uint32_t enumValue) const + { + if (enumValue < m_enumNames.size()) + { + return m_enumNames.at(enumValue); + } + static AZ::Name EmptyName = AZ::Name(); + return EmptyName; + } } // namespace RPI } // namespace AZ diff --git a/Gems/Terrain/Assets/Materials/Terrain/PbrTerrain.materialtype b/Gems/Terrain/Assets/Materials/Terrain/PbrTerrain.materialtype index 01b862c4f8..235079d95e 100644 --- a/Gems/Terrain/Assets/Materials/Terrain/PbrTerrain.materialtype +++ b/Gems/Terrain/Assets/Materials/Terrain/PbrTerrain.materialtype @@ -92,13 +92,58 @@ { "id": "heightmapImage", "displayName": "Heightmap Image", - "description": "Heightmap of the terrain, controlled by the runtime.", + "description": "Heightmap of the terrain. Controlled by the runtime.", + "visibility": "Hidden", "type": "Image", "connection": { "type": "ShaderInput", "id": "m_heightmapImage" } }, + { + "id": "detailMaterialIdImage", + "displayName": "Detail Material Id Image", + "description": "Texture containing detail material Ids and weights. Controlled by the runtime.", + "visibility": "Hidden", + "type": "Image", + "connection": { + "type": "ShaderInput", + "id": "m_detailMaterialIdImage" + } + }, + { + "id": "detailMaterialIdCenter", + "displayName": "Detail Material Id Image Center", + "description": "The center position of the detail material Id image. Controlled by the runtime.", + "visibility": "Hidden", + "type": "Vector2", + "connection": { + "type": "ShaderInput", + "id": "m_detailMaterialIdImageCenter" + } + }, + { + "id": "detailAabb", + "displayName": "Detail material bounds in 2d", + "description": "The 2d world space bounds of the detail id material. Controlled by the runtime.", + "visibility": "Hidden", + "type": "Vector4", + "connection": { + "type": "ShaderInput", + "id": "m_detailAabb" + } + }, + { + "id": "detailHalfPixelUv", + "displayName": "Detail texture half pixel uv size", + "description": "Uv size of a half pixel in the detail material id texture. Controlled by the runtime.", + "visibility": "Hidden", + "type": "float", + "connection": { + "type": "ShaderInput", + "id": "m_detailHalfPixelUv" + } + }, { "id": "detailTextureMultiplier", "displayName": "Detail Texture UV Multiplier", diff --git a/Gems/Terrain/Assets/Shaders/Terrain/TerrainCommon.azsli b/Gems/Terrain/Assets/Shaders/Terrain/TerrainCommon.azsli index 72c1af953c..dc6f65207b 100644 --- a/Gems/Terrain/Assets/Shaders/Terrain/TerrainCommon.azsli +++ b/Gems/Terrain/Assets/Shaders/Terrain/TerrainCommon.azsli @@ -93,10 +93,15 @@ ShaderResourceGroup ObjectSrg : SRG_PerObject ShaderResourceGroup TerrainMaterialSrg : SRG_PerMaterial { Texture2D m_heightmapImage; + Texture2D m_detailMaterialIdImage; + float2 m_detailMaterialIdImageCenter; float m_detailTextureMultiplier; float m_detailFadeDistance; float m_detailFadeLength; + float4 m_detailAabb; + float m_detailHalfPixelUv; + Sampler HeightmapSampler { MinFilter = Linear; @@ -117,6 +122,15 @@ ShaderResourceGroup TerrainMaterialSrg : SRG_PerMaterial MaxAnisotropy = 16; }; + Sampler m_detailSampler + { + AddressU = Wrap; + AddressV = Wrap; + MinFilter = Point; + MagFilter = Point; + MipFilter = Point; + }; + // Base Color float3 m_baseColor; float m_baseColorFactor; diff --git a/Gems/Terrain/Assets/Shaders/Terrain/TerrainPBR_ForwardPass.azsl b/Gems/Terrain/Assets/Shaders/Terrain/TerrainPBR_ForwardPass.azsl index 750cd2fb29..a15ad931f6 100644 --- a/Gems/Terrain/Assets/Shaders/Terrain/TerrainPBR_ForwardPass.azsl +++ b/Gems/Terrain/Assets/Shaders/Terrain/TerrainPBR_ForwardPass.azsl @@ -17,6 +17,7 @@ #include #include #include +#include struct VSOutput { @@ -29,6 +30,8 @@ struct VSOutput float2 m_uv : UV1; }; +option bool o_debugDetailMaterialIds = false; + VSOutput TerrainPBR_MainPassVS(VertexInput IN) { VSOutput OUT; @@ -121,6 +124,48 @@ ForwardPassOutput TerrainPBR_MainPassPS(VSOutput IN) float3 detailColor = GetBaseColorInput(TerrainMaterialSrg::m_baseColorMap, TerrainMaterialSrg::m_sampler, detailUv, TerrainMaterialSrg::m_baseColor.rgb, o_baseColor_useTexture); float3 blendedColor = BlendBaseColor(lerp(detailColor, TerrainMaterialSrg::m_baseColor.rgb, detailFactor), macroColor, TerrainMaterialSrg::m_baseColorFactor, o_baseColorTextureBlendMode, o_baseColor_useTexture); + // ------- Debug detail materials using random colors ------- + // This assigns a random color to each material, turns off any kind of distance fading, and draws a black line at the texture edges. + if (o_debugDetailMaterialIds) + { + float2 detailRegionMin = TerrainMaterialSrg::m_detailAabb.xy; + float2 detailRegionMax = TerrainMaterialSrg::m_detailAabb.zw; + float2 detailRegionUv = (surface.position.xy - detailRegionMin) / (detailRegionMax - detailRegionMin); + if (all(detailRegionUv > TerrainMaterialSrg::m_detailHalfPixelUv) && all(detailRegionUv < 1.0 - TerrainMaterialSrg::m_detailHalfPixelUv)) + { + detailRegionUv += TerrainMaterialSrg::m_detailMaterialIdImageCenter - (0.5); + + uint material1 = TerrainMaterialSrg::m_detailMaterialIdImage.GatherRed(TerrainMaterialSrg::m_detailSampler, detailRegionUv, 0).r; + uint material2 = TerrainMaterialSrg::m_detailMaterialIdImage.GatherGreen(TerrainMaterialSrg::m_detailSampler, detailRegionUv, 0).r; + float blend = float(TerrainMaterialSrg::m_detailMaterialIdImage.GatherBlue(TerrainMaterialSrg::m_detailSampler, detailRegionUv, 0).r) / 0xFF; + + float3 material1Color = float3(0.1, 0.1, 0.1); + float3 material2Color = float3(0.1, 0.1, 0.1); + + // Get a reasonably random hue for the material id + if (material1 != 255) + { + float hue1 = (material1 * 25043 % 256) / 256.0; + material1Color = HsvToRgb(float3(hue1, 1.0, 1.0)); + } + if (material2 != 255) + { + float hue2 = (material2 * 25043 % 256) / 256.0; + material2Color = HsvToRgb(float3(hue2, 1.0, 1.0)); + } + + blendedColor = lerp(material1Color, material2Color, blend); + float seamBlend = 0.0; + const float halfLineWidth = 1.0 / 2048.0; + if (any(abs(detailRegionUv) % 1.0 < halfLineWidth) || any(abs(detailRegionUv) % 1.0 > 1.0 - halfLineWidth)) + { + seamBlend = 1.0; + } + blendedColor = lerp(blendedColor, float3(0.0, 0.0, 0.0), seamBlend); // draw texture seams + blendedColor = pow(blendedColor , 2.2); + } + } + // ------- Specular ------- float specularF0Factor = GetSpecularInput(TerrainMaterialSrg::m_specularF0Map, TerrainMaterialSrg::m_sampler, detailUv, TerrainMaterialSrg::m_specularF0Factor, o_specularF0_useTexture); specularF0Factor = lerp(specularF0Factor, 0.5, detailFactor); diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/Components/TerrainSurfaceMaterialsListComponent.cpp b/Gems/Terrain/Code/Source/TerrainRenderer/Components/TerrainSurfaceMaterialsListComponent.cpp index 271919070c..3c6a213b02 100644 --- a/Gems/Terrain/Code/Source/TerrainRenderer/Components/TerrainSurfaceMaterialsListComponent.cpp +++ b/Gems/Terrain/Code/Source/TerrainRenderer/Components/TerrainSurfaceMaterialsListComponent.cpp @@ -123,20 +123,23 @@ namespace Terrain { surfaceMaterialMapping.m_active = false; surfaceMaterialMapping.m_materialAsset.QueueLoad(); - AZ::Data::AssetBus::Handler::BusConnect(surfaceMaterialMapping.m_materialAsset.GetId()); + AZ::Data::AssetBus::MultiHandler::BusConnect(surfaceMaterialMapping.m_materialAsset.GetId()); } } + + // Announce initial shape using OnShapeChanged + OnShapeChanged(LmbrCentral::ShapeComponentNotifications::ShapeChangeReasons::ShapeChanged); } void TerrainSurfaceMaterialsListComponent::Deactivate() { TerrainAreaMaterialRequestBus::Handler::BusDisconnect(); + AZ::Data::AssetBus::MultiHandler::BusDisconnect(); for (auto& surfaceMaterialMapping : m_configuration.m_surfaceMaterials) { if (surfaceMaterialMapping.m_materialAsset.GetId().IsValid()) { - AZ::Data::AssetBus::Handler::BusDisconnect(surfaceMaterialMapping.m_materialAsset.GetId()); surfaceMaterialMapping.m_materialAsset.Release(); surfaceMaterialMapping.m_materialInstance.reset(); surfaceMaterialMapping.m_activeMaterialAssetId = AZ::Data::AssetId(); @@ -202,7 +205,7 @@ namespace Terrain // Don't disconnect from the AssetBus if this material is mapped more than once. if (CountMaterialIDInstances(surfaceMaterialMapping.m_activeMaterialAssetId) == 1) { - AZ::Data::AssetBus::Handler::BusDisconnect(surfaceMaterialMapping.m_activeMaterialAssetId); + AZ::Data::AssetBus::MultiHandler::BusDisconnect(surfaceMaterialMapping.m_activeMaterialAssetId); } surfaceMaterialMapping.m_activeMaterialAssetId = AZ::Data::AssetId(); @@ -273,12 +276,14 @@ namespace Terrain &TerrainAreaMaterialNotificationBus::Events::OnTerrainSurfaceMaterialMappingRegionChanged, GetEntityId(), oldAabb, m_cachedAabb); } - - const AZStd::vector& TerrainSurfaceMaterialsListComponent::GetSurfaceMaterialMappings( - AZ::Aabb& region) const + + const AZ::Aabb& TerrainSurfaceMaterialsListComponent::GetTerrainSurfaceMaterialRegion() const { - region = m_cachedAabb; + return m_cachedAabb; + } + const AZStd::vector& TerrainSurfaceMaterialsListComponent::GetSurfaceMaterialMappings() const + { return m_configuration.m_surfaceMaterials; } diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/Components/TerrainSurfaceMaterialsListComponent.h b/Gems/Terrain/Code/Source/TerrainRenderer/Components/TerrainSurfaceMaterialsListComponent.h index 7c36033c41..0e32cb12c0 100644 --- a/Gems/Terrain/Code/Source/TerrainRenderer/Components/TerrainSurfaceMaterialsListComponent.h +++ b/Gems/Terrain/Code/Source/TerrainRenderer/Components/TerrainSurfaceMaterialsListComponent.h @@ -53,7 +53,7 @@ namespace Terrain class TerrainSurfaceMaterialsListComponent : public AZ::Component , private TerrainAreaMaterialRequestBus::Handler - , private AZ::Data::AssetBus::Handler + , private AZ::Data::AssetBus::MultiHandler , private LmbrCentral::ShapeComponentNotificationsBus::Handler { public: @@ -86,7 +86,8 @@ namespace Terrain ////////////////////////////////////////////////////////////////////////// // TerrainAreaMaterialRequestBus - const AZStd::vector& GetSurfaceMaterialMappings(AZ::Aabb& region) const override; + const AZ::Aabb& GetTerrainSurfaceMaterialRegion() const override; + const AZStd::vector& GetSurfaceMaterialMappings() const override; ////////////////////////////////////////////////////////////////////////// // AZ::Data::AssetBus::Handler diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainAreaMaterialRequestBus.h b/Gems/Terrain/Code/Source/TerrainRenderer/TerrainAreaMaterialRequestBus.h index dfedf0b34a..ae36a2639e 100644 --- a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainAreaMaterialRequestBus.h +++ b/Gems/Terrain/Code/Source/TerrainRenderer/TerrainAreaMaterialRequestBus.h @@ -9,13 +9,9 @@ #pragma once #include - #include - #include -#include - namespace Terrain { //! This bus provides retrieval of information from Terrain Surfaces. @@ -30,8 +26,11 @@ namespace Terrain virtual ~TerrainAreaMaterialRequests() = default; + //! Get the Aabb for the region where a TerrainSurfaceMaterialMapping exists + virtual const AZ::Aabb& GetTerrainSurfaceMaterialRegion() const = 0; + //! Get the Material asset assigned to a particular surface tag. - virtual const AZStd::vector& GetSurfaceMaterialMappings(AZ::Aabb& region) const = 0; + virtual const AZStd::vector& GetSurfaceMaterialMappings() const = 0; }; using TerrainAreaMaterialRequestBus = AZ::EBus; diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.cpp b/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.cpp index e6aed28897..571d109fd8 100644 --- a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.cpp +++ b/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.cpp @@ -7,11 +7,13 @@ */ #include +#include +#include +#include #include #include #include -#include #include @@ -45,12 +47,49 @@ namespace Terrain { [[maybe_unused]] const char* TerrainFPName = "TerrainFeatureProcessor"; const char* TerrainHeightmapChars = "TerrainHeightmap"; + const char* TerrainDetailChars = "TerrainDetail"; } namespace MaterialInputs { // Terrain material static const char* const HeightmapImage("settings.heightmapImage"); + static const char* const DetailMaterialIdImage("settings.detailMaterialIdImage"); + static const char* const DetailCenter("settings.detailMaterialIdCenter"); + static const char* const DetailAabb("settings.detailAabb"); + static const char* const DetailHalfPixelUv("settings.detailHalfPixelUv"); + } + + namespace DetailMaterialInputs + { + 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 RoughnessUpperBound("roughness.lowerBound"); + static const char* const RoughnessLowerBound("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 @@ -62,6 +101,17 @@ namespace Terrain 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) @@ -78,6 +128,12 @@ namespace Terrain { Initialize(); AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusConnect(); + + m_handleGlobalShaderOptionUpdate = AZ::RPI::ShaderSystemInterface::GlobalShaderOptionUpdatedEvent::Handler + { + [this](const AZ::Name&, AZ::RPI::ShaderOptionValue) { m_forceRebuildDrawPackets = true; } + }; + AZ::RPI::ShaderSystemInterface::Get()->Connect(m_handleGlobalShaderOptionUpdate); } void TerrainFeatureProcessor::Initialize() @@ -139,11 +195,18 @@ namespace Terrain void TerrainFeatureProcessor::OnTerrainDataChanged(const AZ::Aabb& dirtyRegion, TerrainDataChangedMask dataChangedMask) { - if ((dataChangedMask & (TerrainDataChangedMask::HeightData | TerrainDataChangedMask::Settings)) == 0) + if ((dataChangedMask & (TerrainDataChangedMask::HeightData | TerrainDataChangedMask::Settings)) != 0) { - return; + TerrainHeightOrSettingsUpdated(dirtyRegion); + } + if ((dataChangedMask & TerrainDataChangedMask::SurfaceData) != 0) + { + TerrainSurfaceDataUpdated(dirtyRegion); } + } + void TerrainFeatureProcessor::TerrainHeightOrSettingsUpdated(const AZ::Aabb& dirtyRegion) + { AZ::Aabb worldBounds = AZ::Aabb::CreateNull(); AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult( worldBounds, &AzFramework::Terrain::TerrainDataRequests::GetTerrainAabb); @@ -177,10 +240,15 @@ namespace Terrain m_areaData.m_sampleSpacing = queryResolution.GetX(); 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 = FindOrCreateMacroMaterial(entityId); + MacroMaterialData& materialData = FindOrCreateByEntityId(entityId, m_macroMaterials); UpdateMacroMaterialData(materialData, newMaterialData); @@ -197,14 +265,14 @@ namespace Terrain void TerrainFeatureProcessor::OnTerrainMacroMaterialChanged(AZ::EntityId entityId, const MacroMaterialData& newMaterialData) { - MacroMaterialData& data = FindOrCreateMacroMaterial(entityId); + 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 = FindOrCreateMacroMaterial(entityId); + MacroMaterialData& materialData = FindOrCreateByEntityId(entityId, m_macroMaterials); for (SectorData& sectorData : m_sectorData) { bool overlapsOld = sectorData.m_aabb.Overlaps(materialData.m_bounds); @@ -236,7 +304,7 @@ namespace Terrain void TerrainFeatureProcessor::OnTerrainMacroMaterialDestroyed(AZ::EntityId entityId) { - MacroMaterialData* materialData = FindMacroMaterial(entityId); + const MacroMaterialData* materialData = FindByEntityId(entityId, m_macroMaterials); if (materialData) { @@ -255,13 +323,461 @@ namespace Terrain } m_areaData.m_macroMaterialsUpdated = true; - RemoveMacroMaterial(entityId); + RemoveByEntityId(entityId, m_macroMaterials); } - void TerrainFeatureProcessor::UpdateTerrainData() + 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_dirtyDetailRegion.AddAabb(materialRegion.m_region); + } + + 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) + { + 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; + surface.m_detailMaterialId = materialId; + break; + } + } + + if (!found) + { + 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 (DetailMaterialData& detailMaterial : m_detailMaterials.GetDataVector()) + { + if (detailMaterial.m_assetId == material->GetAssetId()) + { + UpdateDetailMaterialData(detailMaterial, material); + detailMaterialId = m_detailMaterials.GetIndexForData(&detailMaterial); + break; + } + } + + if (detailMaterialId == InvalidDetailMaterial) + { + detailMaterialId = m_detailMaterials.GetFreeSlotIndex(); + UpdateDetailMaterialData(m_detailMaterials.GetData(detailMaterialId), material); + } + return detailMaterialId; + } + + void TerrainFeatureProcessor::UpdateDetailMaterialData(DetailMaterialData& materialData, MaterialInstance material) + { + if (materialData.m_materialChangeId != material->GetCurrentChangeId()) + { + materialData = DetailMaterialData(); + DetailTextureFlags& flags = materialData.m_properties.m_flags; + materialData.m_materialChangeId = material->GetCurrentChangeId(); + materialData.m_assetId = material->GetAssetId(); + + 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()) + { + using TypeRefRemoved = AZStd::remove_cvref_t; + ref = material->GetPropertyValue(index).GetValue(); + } + }; + + 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; + applyProperty(BaseColorMap, materialData.m_colorImage); + applyFlag(BaseColorUseTexture, DetailTextureFlags::UseTextureBaseColor); + applyProperty(BaseColorFactor, materialData.m_properties.m_baseColorFactor); + + 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); + } + + applyProperty(MetallicMap, materialData.m_metalnessImage); + applyFlag(MetallicUseTexture, DetailTextureFlags::UseTextureMetallic); + applyProperty(MetallicFactor, materialData.m_properties.m_metalFactor); + + applyProperty(RoughnessMap, materialData.m_roughnessImage); + applyFlag(RoughnessUseTexture, DetailTextureFlags::UseTextureRoughness); + + if ((flags & DetailTextureFlags::UseTextureRoughness) > 0) + { + float lowerBound = 0.0; + float upperBound = 1.0; + applyProperty(RoughnessLowerBound, lowerBound); + applyProperty(RoughnessUpperBound, upperBound); + materialData.m_properties.m_roughnessBias = lowerBound; + materialData.m_properties.m_roughnessScale = upperBound - lowerBound; + } + else + { + materialData.m_properties.m_roughnessBias = 0.0; + applyProperty(RoughnessFactor, materialData.m_properties.m_roughnessScale); + } + + applyProperty(SpecularF0Map, materialData.m_specularF0Image); + applyFlag(SpecularF0UseTexture, DetailTextureFlags::UseTextureSpecularF0); + applyProperty(SpecularF0Factor, materialData.m_properties.m_specularF0Factor); + + applyProperty(NormalMap, materialData.m_normalImage); + applyFlag(NormalUseTexture, DetailTextureFlags::UseTextureNormal); + applyProperty(NormalFactor, materialData.m_properties.m_normalFactor); + applyFlag(NormalFlipX, DetailTextureFlags::FlipNormalX); + applyFlag(NormalFlipY, DetailTextureFlags::FlipNormalY); + + applyProperty(DiffuseOcclusionMap, materialData.m_occlusionImage); + applyFlag(DiffuseOcclusionUseTexture, DetailTextureFlags::UseTextureOcclusion); + applyProperty(DiffuseOcclusionFactor, materialData.m_properties.m_occlusionFactor); + + applyProperty(HeightMap, materialData.m_heightImage); + applyFlag(HeightUseTexture, DetailTextureFlags::UseTextureHeight); + applyProperty(HeightFactor, materialData.m_properties.m_heightFactor); + applyProperty(HeightOffset, materialData.m_properties.m_heightOffset); + applyProperty(HeightBlendFactor, materialData.m_properties.m_heightBlendFactor); + + } + } + + void TerrainFeatureProcessor::CheckUpdateDetailTexture(const Aabb2i& newBounds, const Vector2i& newCenter) { - static const AZ::Name TerrainHeightmapName = AZ::Name(TerrainHeightmapChars); + 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 materialSurface.m_detailMaterialId; + } + } + } + } + return m_detailMaterials.NoFreeSlot; + } + + void TerrainFeatureProcessor::UpdateTerrainData() + { uint32_t width = m_areaData.m_updateWidth; uint32_t height = m_areaData.m_updateHeight; const AZ::Aabb& worldBounds = m_areaData.m_terrainBounds; @@ -280,8 +796,10 @@ namespace Terrain AZ::RHI::ImageDescriptor imageDescriptor = AZ::RHI::ImageDescriptor::Create2D( AZ::RHI::ImageBindFlags::ShaderRead, width, height, AZ::RHI::Format::R16_UNORM ); + + 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!"); + AZ_Error(TerrainFPName, m_areaData.m_heightmapImage, "Failed to initialize the heightmap image."); } AZStd::vector pixels; @@ -366,6 +884,19 @@ namespace Terrain m_heightmapPropertyIndex = m_materialInstance->GetMaterialPropertiesLayout()->FindPropertyIndex(AZ::Name(MaterialInputs::HeightmapImage)); AZ_Error(TerrainFPName, m_heightmapPropertyIndex.IsValid(), "Failed to find material input constant %s.", MaterialInputs::HeightmapImage); + m_detailMaterialIdPropertyIndex = m_materialInstance->GetMaterialPropertiesLayout()->FindPropertyIndex(AZ::Name(MaterialInputs::DetailMaterialIdImage)); + AZ_Error(TerrainFPName, m_detailMaterialIdPropertyIndex.IsValid(), "Failed to find material input constant %s.", MaterialInputs::DetailMaterialIdImage); + + m_detailCenterPropertyIndex = m_materialInstance->GetMaterialPropertiesLayout()->FindPropertyIndex(AZ::Name(MaterialInputs::DetailCenter)); + AZ_Error(TerrainFPName, m_detailCenterPropertyIndex.IsValid(), "Failed to find material input constant %s.", MaterialInputs::DetailCenter); + + m_detailAabbPropertyIndex = m_materialInstance->GetMaterialPropertiesLayout()->FindPropertyIndex(AZ::Name(MaterialInputs::DetailAabb)); + AZ_Error(TerrainFPName, m_detailAabbPropertyIndex.IsValid(), "Failed to find material input constant %s.", MaterialInputs::DetailAabb); + + m_detailHalfPixelUvPropertyIndex = m_materialInstance->GetMaterialPropertiesLayout()->FindPropertyIndex(AZ::Name(MaterialInputs::DetailHalfPixelUv)); + AZ_Error(TerrainFPName, m_detailHalfPixelUvPropertyIndex.IsValid(), "Failed to find material input constant %s.", MaterialInputs::DetailHalfPixelUv); + + // Find any macro materials that have already been created. TerrainMacroMaterialRequestBus::EnumerateHandlers( [&](TerrainMacroMaterialRequests* handler) { @@ -376,6 +907,30 @@ namespace Terrain } ); 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) @@ -474,14 +1029,73 @@ namespace Terrain } } } + 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(); - - const AZ::Data::Instance heightmapImage = m_areaData.m_heightmapImage; + + const AZ::Data::Instance heightmapImage = m_areaData.m_heightmapImage; // cast StreamingImage to Image m_materialInstance->SetPropertyValue(m_heightmapPropertyIndex, heightmapImage); - m_materialInstance->Compile(); + } + + AZ::Vector3 cameraPosition = AZ::Vector3::CreateZero(); + for (auto& view : process.m_views) + { + if ((view->GetUsageFlags() & AZ::RPI::View::UsageFlags::UsageCamera) > 0) + { + cameraPosition = view->GetCameraTransform().GetTranslation(); + break; + } + } + + if (m_dirtyDetailRegion.IsValid() || !cameraPosition.IsClose(m_previousCameraPosition)) + { + 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; + const AZ::Data::Instance detailTextureImage = m_detailTextureImage; // cast StreamingImage to Image + m_materialInstance->SetPropertyValue(m_detailMaterialIdPropertyIndex, detailTextureImage); + + 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 + ); + m_materialInstance->SetPropertyValue(m_detailAabbPropertyIndex, detailAabb); + m_materialInstance->SetPropertyValue(m_detailHalfPixelUvPropertyIndex, 0.5f / DetailTextureSize); + + AZ::Vector2 detailUvOffset = AZ::Vector2(float(newCenter.m_x) / DetailTextureSize, float(newCenter.m_y) / DetailTextureSize); + m_materialInstance->SetPropertyValue(m_detailCenterPropertyIndex, detailUvOffset); } if (m_areaData.m_heightmapUpdated || m_areaData.m_macroMaterialsUpdated) @@ -603,6 +1217,11 @@ namespace Terrain } } } + + if (m_materialInstance) + { + m_materialInstance->Compile(); + } } void TerrainFeatureProcessor::InitializeTerrainPatch(uint16_t gridSize, float gridSpacing, PatchData& patchdata) @@ -746,9 +1365,10 @@ namespace Terrain // larger but this will limit how much is rendered. } - MacroMaterialData* TerrainFeatureProcessor::FindMacroMaterial(AZ::EntityId entityId) + template + T* TerrainFeatureProcessor::FindByEntityId(AZ::EntityId entityId, AZ::Render::IndexedDataVector& container) { - for (MacroMaterialData& data : m_macroMaterials.GetDataVector()) + for (T& data : container.GetDataVector()) { if (data.m_entityId == entityId) { @@ -757,34 +1377,36 @@ namespace Terrain } return nullptr; } - - MacroMaterialData& TerrainFeatureProcessor::FindOrCreateMacroMaterial(AZ::EntityId entityId) + + template + T& TerrainFeatureProcessor::FindOrCreateByEntityId(AZ::EntityId entityId, AZ::Render::IndexedDataVector& container) { - MacroMaterialData* dataPtr = FindMacroMaterial(entityId); + T* dataPtr = FindByEntityId(entityId, container); if (dataPtr != nullptr) { return *dataPtr; } - const uint16_t slotId = m_macroMaterials.GetFreeSlotIndex(); - AZ_Assert(slotId != m_macroMaterials.NoFreeSlot, "Ran out of indices for macro materials"); + const uint16_t slotId = container.GetFreeSlotIndex(); + AZ_Assert(slotId != AZ::Render::IndexedDataVector::NoFreeSlot, "Ran out of indices"); - MacroMaterialData& data = m_macroMaterials.GetData(slotId); + T& data = container.GetData(slotId); data.m_entityId = entityId; return data; } - - void TerrainFeatureProcessor::RemoveMacroMaterial(AZ::EntityId entityId) + + template + void TerrainFeatureProcessor::RemoveByEntityId(AZ::EntityId entityId, AZ::Render::IndexedDataVector& container) { - for (MacroMaterialData& data : m_macroMaterials.GetDataVector()) + for (T& data : container.GetDataVector()) { if (data.m_entityId == entityId) { - m_macroMaterials.RemoveData(&data); + container.RemoveData(&data); return; } } - AZ_Assert(false, "Entity Id not found in m_macroMaterials.") + AZ_Assert(false, "Entity Id not found in container.") } template @@ -798,4 +1420,60 @@ 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 f82fd8ecb0..962ffe13bf 100644 --- a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.h +++ b/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.h @@ -12,11 +12,13 @@ #include #include +#include #include #include #include #include +#include #include namespace AZ::RPI @@ -37,6 +39,7 @@ namespace Terrain , 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); @@ -112,6 +115,109 @@ namespace Terrain 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'0000'0000'0000'1000'0000, + FlipNormalY = 0b0000'0000'0000'0000'0000'0001'0000'0000, + + BlendModeMask = 0b0000'0000'0000'0000'0000'0110'0000'0000, + BlendModeLerp = 0b0000'0000'0000'0000'0000'0000'0000'0000, + BlendModeLinearLight = 0b0000'0000'0000'0000'0000'0010'0000'0000, + BlendModeMultiply = 0b0000'0000'0000'0000'0000'0100'0000'0000, + BlendModeOverlay = 0b0000'0000'0000'0000'0000'0110'0000'0000, + }; + + struct DetailMaterialShaderProperties + { + // 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, + }; + + // 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 }; + + float m_padding; // 16 byte aligned + }; + + struct DetailMaterialData + { + AZ::Data::AssetId m_assetId; + AZ::RPI::Material::ChangeId m_materialChangeId{AZ::RPI::Material::DEFAULT_CHANGE_ID}; + + 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; + + DetailMaterialShaderProperties m_properties; // maps directly to shader + }; + + 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; + }; + // AZ::RPI::MaterialReloadNotificationBus::Handler overrides... void OnMaterialReinitialized(const MaterialInstance& material) override; @@ -124,6 +230,12 @@ namespace Terrain 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; void Initialize(); void InitializeTerrainPatch(uint16_t gridSize, float gridSpacing, PatchData& patchdata); @@ -132,12 +244,26 @@ namespace Terrain void UpdateTerrainData(); 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 UpdateDetailMaterialData(DetailMaterialData& materialData, 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 ProcessSurfaces(const FeatureProcessor::RenderPacket& process); - - MacroMaterialData* FindMacroMaterial(AZ::EntityId entityId); - MacroMaterialData& FindOrCreateMacroMaterial(AZ::EntityId entityId); - void RemoveMacroMaterial(AZ::EntityId entityId); + + 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); @@ -147,8 +273,11 @@ namespace Terrain // System-level parameters static constexpr float GridSpacing{ 1.0f }; - static constexpr uint32_t GridSize{ 64 }; // number of terrain quads (vertices are m_gridSize + 1) + 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 }; AZStd::unique_ptr m_materialAssetLoader; MaterialInstance m_materialInstance; @@ -160,8 +289,13 @@ namespace Terrain AZ::RHI::ShaderInputImageIndex m_macroColorMapIndex; AZ::RHI::ShaderInputImageIndex m_macroNormalMapIndex; AZ::RPI::MaterialPropertyIndex m_heightmapPropertyIndex; + AZ::RPI::MaterialPropertyIndex m_detailMaterialIdPropertyIndex; + AZ::RPI::MaterialPropertyIndex m_detailCenterPropertyIndex; + AZ::RPI::MaterialPropertyIndex m_detailAabbPropertyIndex; + AZ::RPI::MaterialPropertyIndex m_detailHalfPixelUvPropertyIndex; AZ::Data::Instance m_patchModel; + AZ::Vector3 m_previousCameraPosition = AZ::Vector3(AZStd::numeric_limits::max(), 0.0, 0.0); // Per-area data struct TerrainAreaData @@ -178,12 +312,21 @@ namespace Terrain bool m_macroMaterialsUpdated{ true }; bool m_rebuildSectors{ true }; }; - + TerrainAreaData m_areaData; AZ::Aabb m_dirtyRegion{ AZ::Aabb::CreateNull() }; + AZ::Aabb m_dirtyDetailRegion{ AZ::Aabb::CreateNull() }; + + Aabb2i m_detailTextureBounds; + Vector2i m_detailTextureCenter; + AZ::Data::Instance m_detailTextureImage; + AZ::RPI::ShaderSystemInterface::GlobalShaderOptionUpdatedEvent::Handler m_handleGlobalShaderOptionUpdate; + bool m_forceRebuildDrawPackets = false; AZStd::vector m_sectorData; AZ::Render::IndexedDataVector m_macroMaterials; + AZ::Render::IndexedDataVector m_detailMaterials; + AZ::Render::IndexedDataVector m_detailMaterialRegions; }; }