diff --git a/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.cpp b/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.cpp index 06372a3490..1c296b2e2c 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.cpp +++ b/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.cpp @@ -105,17 +105,19 @@ namespace Terrain AZ::TransformNotificationBus::Handler::BusConnect(GetEntityId()); LmbrCentral::ShapeComponentNotificationsBus::Handler::BusConnect(GetEntityId()); TerrainAreaRequestBus::Handler::BusConnect(GetEntityId()); + TerrainSpawnerRequestBus::Handler::BusConnect(GetEntityId()); TerrainSystemServiceRequestBus::Broadcast(&TerrainSystemServiceRequestBus::Events::RegisterArea, GetEntityId()); } void TerrainLayerSpawnerComponent::Deactivate() { - TerrainAreaRequestBus::Handler::BusDisconnect(); TerrainSystemServiceRequestBus::Broadcast(&TerrainSystemServiceRequestBus::Events::UnregisterArea, GetEntityId()); - - AZ::TransformNotificationBus::Handler::BusDisconnect(); + TerrainSpawnerRequestBus::Handler::BusDisconnect(); + TerrainAreaRequestBus::Handler::BusDisconnect(); LmbrCentral::ShapeComponentNotificationsBus::Handler::BusDisconnect(); + AZ::TransformNotificationBus::Handler::BusDisconnect(); + } bool TerrainLayerSpawnerComponent::ReadInConfig(const AZ::ComponentConfig* baseConfig) @@ -147,6 +149,17 @@ namespace Terrain { RefreshArea(); } + + void TerrainLayerSpawnerComponent::GetPriority(AZ::u32& outLayer, AZ::u32& outPriority) + { + outLayer = m_configuration.m_layer; + outPriority = m_configuration.m_priority; + } + + bool TerrainLayerSpawnerComponent::GetUseGroundPlane() + { + return m_configuration.m_useGroundPlane; + } void TerrainLayerSpawnerComponent::RegisterArea() { diff --git a/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.h b/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.h index 1f1ae90227..3f8e72e1b8 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.h +++ b/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.h @@ -59,6 +59,7 @@ namespace Terrain , private AZ::TransformNotificationBus::Handler , private LmbrCentral::ShapeComponentNotificationsBus::Handler , private Terrain::TerrainAreaRequestBus::Handler + , private Terrain::TerrainSpawnerRequestBus::Handler { public: template @@ -80,7 +81,6 @@ namespace Terrain bool ReadInConfig(const AZ::ComponentConfig* baseConfig) override; bool WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const override; - ////////////////////////////////////////////////////////////////////////// // AZ::TransformNotificationBus::Handler void OnTransformChanged(const AZ::Transform& local, const AZ::Transform& world) override; @@ -88,6 +88,10 @@ namespace Terrain // ShapeComponentNotificationsBus void OnShapeChanged(ShapeChangeReasons changeReason) override; + // TerrainSpawnerRequestBus + void GetPriority(AZ::u32& outLayer, AZ::u32& outPriority) override; + bool GetUseGroundPlane() override; + void RegisterArea() override; void RefreshArea() override; diff --git a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp index 90b74f08ba..a271d624f5 100644 --- a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp +++ b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp @@ -17,6 +17,33 @@ using namespace Terrain; +bool TerrainLayerPriorityComparator::operator()(const AZ::EntityId& layer1id, const AZ::EntityId& layer2id) const +{ + // Comparator for insertion/keylookup. + // Sorts into layer/priority order, highest priority first. + AZ::u32 priority1, layer1; + Terrain::TerrainSpawnerRequestBus::Event(layer1id, &Terrain::TerrainSpawnerRequestBus::Events::GetPriority, layer1, priority1); + + AZ::u32 priority2, layer2; + Terrain::TerrainSpawnerRequestBus::Event(layer2id, &Terrain::TerrainSpawnerRequestBus::Events::GetPriority, layer2, priority2); + + if (layer1 < layer2) + { + return false; + } + else if (layer1 > layer2) + { + return true; + } + + if (priority1 != priority2) + { + return priority1 > priority2; + } + + return layer1id > layer2id; +} + TerrainSystem::TerrainSystem() { Terrain::TerrainSystemServiceRequestBus::Handler::BusConnect(); @@ -78,17 +105,14 @@ float TerrainSystem::GetHeightSynchronous(float x, float y) const AZStd::shared_lock lock(m_areaMutex); - if (!m_registeredAreas.empty()) + for (auto& [areaId, areaBounds] : m_registeredAreas) { - for (auto& [areaId, areaBounds] : m_registeredAreas) + inPosition.SetZ(areaBounds.GetMin().GetZ()); + if (areaBounds.Contains(inPosition)) { - inPosition.SetZ(areaBounds.GetMin().GetZ()); - if (areaBounds.Contains(inPosition)) - { - Terrain::TerrainAreaHeightRequestBus::Event( - areaId, &Terrain::TerrainAreaHeightRequestBus::Events::GetHeight, inPosition, outPosition, - Terrain::TerrainAreaHeightRequestBus::Events::Sampler::DEFAULT); - } + Terrain::TerrainAreaHeightRequestBus::Event( + areaId, &Terrain::TerrainAreaHeightRequestBus::Events::GetHeight, inPosition, outPosition, + Terrain::TerrainAreaHeightRequestBus::Events::Sampler::DEFAULT); } } @@ -305,26 +329,34 @@ void TerrainSystem::SystemDeactivate() void TerrainSystem::RegisterArea(AZ::EntityId areaId) { - { - AZStd::unique_lock lock(m_areaMutex); - AZ::Aabb aabb = AZ::Aabb::CreateNull(); - LmbrCentral::ShapeComponentRequestsBus::EventResult(aabb, areaId, &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb); - m_registeredAreas[areaId] = aabb; - } - - RefreshArea(areaId); + AZStd::unique_lock lock(m_areaMutex); + AZ::Aabb aabb = AZ::Aabb::CreateNull(); + LmbrCentral::ShapeComponentRequestsBus::EventResult(aabb, areaId, &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb); + m_registeredAreas[areaId] = aabb; + m_dirtyRegion.AddAabb(aabb); + m_terrainHeightDirty = true; } void TerrainSystem::UnregisterArea(AZ::EntityId areaId) { - { - AZStd::unique_lock lock(m_areaMutex); - AZ::Aabb aabb = AZ::Aabb::CreateNull(); - LmbrCentral::ShapeComponentRequestsBus::EventResult(aabb, areaId, &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb); - m_registeredAreas.erase(areaId); - } + AZStd::unique_lock lock(m_areaMutex); - RefreshArea(areaId); + // Remove the data for this entity from the registered areas. + // Erase_if is used as erase would use the comparator to lookup the entity id in the map. + // As the comparator will get the new layer/priority data for the entity, the id lookup will fail. + AZStd::erase_if( + m_registeredAreas, + [areaId, this](const auto& item) + { + auto const& [entityId, aabb] = item; + if (areaId == entityId) + { + m_dirtyRegion.AddAabb(aabb); + m_terrainHeightDirty = true; + return true; + } + return false; + }); } void TerrainSystem::RefreshArea(AZ::EntityId areaId) @@ -336,7 +368,6 @@ void TerrainSystem::RefreshArea(AZ::EntityId areaId) AZ::Aabb oldAabb = (areaAabb != m_registeredAreas.end()) ? areaAabb->second : AZ::Aabb::CreateNull(); AZ::Aabb newAabb = AZ::Aabb::CreateNull(); LmbrCentral::ShapeComponentRequestsBus::EventResult(newAabb, areaId, &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb); - m_registeredAreas[areaId] = newAabb; AZ::Aabb expandedAabb = oldAabb; @@ -400,31 +431,37 @@ void TerrainSystem::OnTick(float /*deltaTime*/, AZ::ScriptTimePoint /*time*/) const uint32_t pixelDataSize = width * height * sizeof(float); memset(pixels.data(), 0, pixelDataSize); - for (auto& [areaId, areaBounds] : m_registeredAreas) + for (uint32_t y = 0; y < height; y++) { - for (uint32_t y = 0; y < height; y++) + for (uint32_t x = 0; x < width; x++) { - for (uint32_t x = 0; x < width; x++) + // Find the first terrain layer that covers this position. This will be the highest priority, so others can be ignored. + for (auto& [areaId, areaBounds] : m_registeredAreas) { AZ::Vector3 inPosition( (x * m_currentSettings.m_heightQueryResolution.GetX()) + m_currentSettings.m_worldBounds.GetMin().GetX(), (y * m_currentSettings.m_heightQueryResolution.GetY()) + m_currentSettings.m_worldBounds.GetMin().GetY(), areaBounds.GetMin().GetZ()); - if (areaBounds.Contains(inPosition)) + + if (!areaBounds.Contains(inPosition)) { - AZ::Vector3 outPosition; - const Terrain::TerrainAreaHeightRequests::Sampler sampleFilter = - Terrain::TerrainAreaHeightRequests::Sampler::DEFAULT; - - Terrain::TerrainAreaHeightRequestBus::Event( - areaId, &Terrain::TerrainAreaHeightRequestBus::Events::GetHeight, inPosition, outPosition, sampleFilter); - - pixels[(y * width) + x] = (outPosition.GetZ() - m_currentSettings.m_worldBounds.GetMin().GetZ()) / - m_currentSettings.m_worldBounds.GetExtents().GetZ(); + continue; } + + AZ::Vector3 outPosition; + const Terrain::TerrainAreaHeightRequests::Sampler sampleFilter = Terrain::TerrainAreaHeightRequests::Sampler::DEFAULT; + + Terrain::TerrainAreaHeightRequestBus::Event( + areaId, &Terrain::TerrainAreaHeightRequestBus::Events::GetHeight, inPosition, outPosition, sampleFilter); + + pixels[(y * width) + x] = (outPosition.GetZ() - m_currentSettings.m_worldBounds.GetMin().GetZ()) / + m_currentSettings.m_worldBounds.GetExtents().GetZ(); + + break; } } } + const AZ::RPI::Scene* scene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene().get(); auto terrainFeatureProcessor = scene->GetFeatureProcessor(); diff --git a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h index 2d2286a0c3..0239170640 100644 --- a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h +++ b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h @@ -25,6 +25,11 @@ namespace Terrain { + struct TerrainLayerPriorityComparator + { + bool operator()(const AZ::EntityId& layer1id, const AZ::EntityId& layer2id) const; + }; + class TerrainSystem : public AzFramework::Terrain::TerrainDataRequestBus::Handler , private Terrain::TerrainSystemServiceRequestBus::Handler @@ -112,6 +117,6 @@ namespace Terrain AZ::Aabb m_dirtyRegion; mutable AZStd::shared_mutex m_areaMutex; - AZStd::unordered_map m_registeredAreas; + AZStd::map m_registeredAreas; }; } // namespace Terrain diff --git a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystemBus.h b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystemBus.h index e999cbf8be..cb41ba9957 100644 --- a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystemBus.h +++ b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystemBus.h @@ -112,5 +112,26 @@ namespace Terrain }; using TerrainAreaHeightRequestBus = AZ::EBus; + + /** + * A bus for the TerrainSystem to interrogate TerrainLayerSpawners. + */ + class TerrainSpawnerRequests + : public AZ::ComponentBus + { + public: + //////////////////////////////////////////////////////////////////////// + // EBusTraits + using MutexType = AZStd::recursive_mutex; + //////////////////////////////////////////////////////////////////////// + + virtual ~TerrainSpawnerRequests() = default; + + virtual void GetPriority(AZ::u32& outLayer, AZ::u32& outPriority) = 0; + virtual bool GetUseGroundPlane() = 0; + + }; + + using TerrainSpawnerRequestBus = AZ::EBus; } diff --git a/Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp b/Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp new file mode 100644 index 0000000000..91d6a26f75 --- /dev/null +++ b/Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp @@ -0,0 +1,222 @@ +/* + * 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 + +class LayerSpawnerComponentTest + : public ::testing::Test +{ +protected: + AZ::ComponentApplication m_app; + + AZStd::unique_ptr m_entity; + Terrain::TerrainLayerSpawnerComponent* m_layerSpawnerComponent; + UnitTest::MockBoxShapeComponent* m_shapeComponent; + AZStd::unique_ptr m_terrainSystem; + + void SetUp() override + { + AZ::ComponentApplication::Descriptor appDesc; + appDesc.m_memoryBlocksByteSize = 20 * 1024 * 1024; + appDesc.m_recordingMode = AZ::Debug::AllocationRecords::RECORD_NO_RECORDS; + appDesc.m_stackRecordLevels = 20; + + m_app.Create(appDesc); + } + + void TearDown() override + { + if (m_terrainSystem) + { + m_terrainSystem->Deactivate(); + } + m_app.Destroy(); + } + + void CreateEntity() + { + m_entity = AZStd::make_unique(); + m_entity->Init(); + + ASSERT_TRUE(m_entity); + } + + void AddLayerSpawnerAndShapeComponentToEntity() + { + AddLayerSpawnerAndShapeComponentToEntity(Terrain::TerrainLayerSpawnerConfig()); + } + + void AddLayerSpawnerAndShapeComponentToEntity(const Terrain::TerrainLayerSpawnerConfig& config) + { + m_layerSpawnerComponent = m_entity->CreateComponent(config); + m_app.RegisterComponentDescriptor(m_layerSpawnerComponent->CreateDescriptor()); + + m_shapeComponent = m_entity->CreateComponent(); + m_app.RegisterComponentDescriptor(m_shapeComponent->CreateDescriptor()); + + ASSERT_TRUE(m_layerSpawnerComponent); + ASSERT_TRUE(m_shapeComponent); + } + + void ResetEntity() + { + m_entity->Deactivate(); + m_entity->Reset(); + } + + void CreateMockTerrainSystem() + { + m_terrainSystem = AZStd::make_unique(); + m_terrainSystem->Activate(); + } +}; + +TEST_F(LayerSpawnerComponentTest, ActivatEntityActivateSuccess) +{ + CreateEntity(); + AddLayerSpawnerAndShapeComponentToEntity(); + + m_entity->Activate(); + EXPECT_EQ(m_entity->GetState(), AZ::Entity::State::Active); + + ResetEntity(); +} + +TEST_F(LayerSpawnerComponentTest, LayerSpawnerDefaultValuesCorrect) +{ + CreateEntity(); + AddLayerSpawnerAndShapeComponentToEntity(); + + m_entity->Activate(); + + AZ::u32 priority = 999, layer = 999; + Terrain::TerrainSpawnerRequestBus::Event(m_entity->GetId(), &Terrain::TerrainSpawnerRequestBus::Events::GetPriority, layer, priority); + + EXPECT_EQ(0, priority); + EXPECT_EQ(1, layer); + + bool useGroundPlane = false; + + Terrain::TerrainSpawnerRequestBus::EventResult(useGroundPlane, m_entity->GetId(), &Terrain::TerrainSpawnerRequestBus::Events::GetUseGroundPlane); + + EXPECT_TRUE(useGroundPlane); + + ResetEntity(); +} + +TEST_F(LayerSpawnerComponentTest, LayerSpawnerConfigValuesCorrect) +{ + CreateEntity(); + + constexpr static AZ::u32 testPriority = 15; + constexpr static AZ::u32 testLayer = 0; + + Terrain::TerrainLayerSpawnerConfig config; + config.m_layer = testLayer; + config.m_priority = testPriority; + config.m_useGroundPlane = false; + + AddLayerSpawnerAndShapeComponentToEntity(config); + + m_entity->Activate(); + + AZ::u32 priority = 999, layer = 999; + Terrain::TerrainSpawnerRequestBus::Event(m_entity->GetId(), &Terrain::TerrainSpawnerRequestBus::Events::GetPriority, layer, priority); + + EXPECT_EQ(testPriority, priority); + EXPECT_EQ(testLayer, layer); + + bool useGroundPlane = true; + + Terrain::TerrainSpawnerRequestBus::EventResult( + useGroundPlane, m_entity->GetId(), &Terrain::TerrainSpawnerRequestBus::Events::GetUseGroundPlane); + + EXPECT_FALSE(useGroundPlane); + + ResetEntity(); +} + +TEST_F(LayerSpawnerComponentTest, LayerSpawnerRegisterAreaUpdatesTerrainSystem) +{ + CreateEntity(); + + CreateMockTerrainSystem(); + + AddLayerSpawnerAndShapeComponentToEntity(); + + m_entity->Activate(); + + // The Activate call should have registered the area. + EXPECT_EQ(1, m_terrainSystem->m_registerAreaCalledCount); + + ResetEntity(); +} + +TEST_F(LayerSpawnerComponentTest, LayerSpawnerUnregisterAreaUpdatesTerrainSystem) +{ + CreateEntity(); + + CreateMockTerrainSystem(); + + AddLayerSpawnerAndShapeComponentToEntity(); + + m_entity->Activate(); + + m_layerSpawnerComponent->Deactivate(); + + // The Deactivate call should have unregistered the area. + EXPECT_EQ(1, m_terrainSystem->m_unregisterAreaCalledCount); + + ResetEntity(); +} + +TEST_F(LayerSpawnerComponentTest, LayerSpawnerTransformChangedUpdatesTerrainSystem) +{ + CreateEntity(); + + CreateMockTerrainSystem(); + + AddLayerSpawnerAndShapeComponentToEntity(); + + m_entity->Activate(); + + AZ::TransformNotificationBus::Event( + m_entity->GetId(), &AZ::TransformNotificationBus::Events::OnTransformChanged, AZ::Transform(), AZ::Transform()); + + EXPECT_EQ(1, m_terrainSystem->m_refreshAreaCalledCount); + + ResetEntity(); +} + +TEST_F(LayerSpawnerComponentTest, LayerSpawnerShapeChangedUpdatesTerrainSystem) +{ + CreateEntity(); + + CreateMockTerrainSystem(); + + AddLayerSpawnerAndShapeComponentToEntity(); + + m_entity->Activate(); + + LmbrCentral::ShapeComponentNotificationsBus::Event( + m_entity->GetId(), &LmbrCentral::ShapeComponentNotificationsBus::Events::OnShapeChanged, + LmbrCentral::ShapeComponentNotifications::ShapeChangeReasons::ShapeChanged); + + EXPECT_EQ(1, m_terrainSystem->m_refreshAreaCalledCount); + + ResetEntity(); +} diff --git a/Gems/Terrain/Code/Tests/TerrainMocks.h b/Gems/Terrain/Code/Tests/TerrainMocks.h new file mode 100644 index 0000000000..5f90cafd69 --- /dev/null +++ b/Gems/Terrain/Code/Tests/TerrainMocks.h @@ -0,0 +1,104 @@ +/* + * 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 UnitTest +{ + static const AZ::Uuid BoxShapeComponentTypeId = "{5EDF4B9E-0D3D-40B8-8C91-5142BCFC30A6}"; + + class MockBoxShapeComponent + : public AZ::Component + { + public: + AZ_COMPONENT(MockBoxShapeComponent, BoxShapeComponentTypeId) + static void Reflect([[maybe_unused]] AZ::ReflectContext* context) + { + } + + void Activate() override + { + } + + void Deactivate() override + { + } + + bool ReadInConfig([[maybe_unused]] const AZ::ComponentConfig* baseConfig) override + { + return true; + } + + bool WriteOutConfig([[maybe_unused]] AZ::ComponentConfig* outBaseConfig) const override + { + return true; + } + + private: + static void GetProvidedServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.push_back(AZ_CRC("ShapeService", 0xe86aa5fe)); + provided.push_back(AZ_CRC("BoxShapeService", 0x946a0032)); + } + + static void GetIncompatibleServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + } + + static void GetRequiredServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& required) + { + } + + static void GetDependentServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& dependent) + { + } + }; + + class MockTerrainSystem : private Terrain::TerrainSystemServiceRequestBus::Handler + { + public: + void Activate() override + { + Terrain::TerrainSystemServiceRequestBus::Handler::BusConnect(); + } + + void Deactivate() override + { + Terrain::TerrainSystemServiceRequestBus::Handler::BusDisconnect(); + } + + void SetWorldBounds(const AZ::Aabb& worldBounds) override + { + } + + void SetHeightQueryResolution([[maybe_unused]] AZ::Vector2 queryResolution) override + { + } + + void RegisterArea([[maybe_unused]] AZ::EntityId areaId) override + { + m_registerAreaCalledCount++; + } + + void UnregisterArea([[maybe_unused]] AZ::EntityId areaId) override + { + m_unregisterAreaCalledCount++; + } + + void RefreshArea([[maybe_unused]] AZ::EntityId areaId) override + { + m_refreshAreaCalledCount++; + } + + int m_registerAreaCalledCount = 0; + int m_refreshAreaCalledCount = 0; + int m_unregisterAreaCalledCount = 0; + }; +} diff --git a/Gems/Terrain/Code/terrain_tests_files.cmake b/Gems/Terrain/Code/terrain_tests_files.cmake index beed6bd83d..b44f143f3b 100644 --- a/Gems/Terrain/Code/terrain_tests_files.cmake +++ b/Gems/Terrain/Code/terrain_tests_files.cmake @@ -7,5 +7,7 @@ # set(FILES + Tests/TerrainMocks.h Tests/TerrainTest.cpp + Tests/LayerSpawnerTests.cpp )