diff --git a/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp index 82568126df..3fcda71998 100644 --- a/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp @@ -85,7 +85,6 @@ namespace AZ { const auto jobLambda = [&]() -> void { - AZ_PROFILE_SCOPE(AzRender, "MeshFP::Simulate() Lambda"); for (auto meshDataIter = iteratorRange.first; meshDataIter != iteratorRange.second; ++meshDataIter) { if (!meshDataIter->m_model) diff --git a/Gems/Terrain/Code/Source/Components/TerrainSystemComponent.cpp b/Gems/Terrain/Code/Source/Components/TerrainSystemComponent.cpp index afaf0470b4..5357ccce32 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainSystemComponent.cpp +++ b/Gems/Terrain/Code/Source/Components/TerrainSystemComponent.cpp @@ -13,8 +13,6 @@ #include #include -#include -#include #include namespace Terrain @@ -34,8 +32,6 @@ namespace Terrain ->Attribute(AZ::Edit::Attributes::AutoExpand, true) ; } - - Terrain::TerrainFeatureProcessor::Reflect(context); } } @@ -49,9 +45,8 @@ namespace Terrain incompatible.push_back(AZ_CRC_CE("TerrainService")); } - void TerrainSystemComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required) + void TerrainSystemComponent::GetRequiredServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& required) { - required.push_back(AZ_CRC_CE("RPISystem")); } void TerrainSystemComponent::GetDependentServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& dependent) @@ -68,14 +63,11 @@ namespace Terrain // every time an entity is added or removed to a level. If this ever changes, the Terrain System ownership could move into // the level component. m_terrainSystem = new TerrainSystem(); - AZ::RPI::FeatureProcessorFactory::Get()->RegisterFeatureProcessor(); } void TerrainSystemComponent::Deactivate() { delete m_terrainSystem; m_terrainSystem = nullptr; - - AZ::RPI::FeatureProcessorFactory::Get()->UnregisterFeatureProcessor(); } } diff --git a/Gems/Terrain/Code/Source/Components/TerrainWorldDebuggerComponent.cpp b/Gems/Terrain/Code/Source/Components/TerrainWorldDebuggerComponent.cpp index 7233d08281..e140129563 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainWorldDebuggerComponent.cpp +++ b/Gems/Terrain/Code/Source/Components/TerrainWorldDebuggerComponent.cpp @@ -95,6 +95,8 @@ namespace Terrain AzFramework::EntityDebugDisplayEventBus::Handler::BusConnect(GetEntityId()); AzFramework::BoundsRequestBus::Handler::BusConnect(GetEntityId()); AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusConnect(); + + RefreshCachedWireframeGrid(AZ::Aabb::CreateNull()); } void TerrainWorldDebuggerComponent::Deactivate() diff --git a/Gems/Terrain/Code/Source/Components/TerrainWorldRendererComponent.cpp b/Gems/Terrain/Code/Source/Components/TerrainWorldRendererComponent.cpp new file mode 100644 index 0000000000..3bf34fa019 --- /dev/null +++ b/Gems/Terrain/Code/Source/Components/TerrainWorldRendererComponent.cpp @@ -0,0 +1,213 @@ +/* + * 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 + +namespace Terrain +{ + void TerrainWorldRendererConfig::Reflect(AZ::ReflectContext* context) + { + Terrain::TerrainFeatureProcessor::Reflect(context); + + AZ::SerializeContext* serialize = azrtti_cast(context); + if (serialize) + { + serialize->Class()->Version(1); + + AZ::EditContext* edit = serialize->GetEditContext(); + if (edit) + { + edit->Class("Terrain World Renderer Component", "Enables terrain rendering") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZStd::vector({ AZ_CRC_CE("Level") })) + ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) + ->Attribute(AZ::Edit::Attributes::AutoExpand, true); + } + } + } + + void TerrainWorldRendererComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services) + { + services.push_back(AZ_CRC_CE("TerrainRendererService")); + } + + void TerrainWorldRendererComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& services) + { + services.push_back(AZ_CRC_CE("TerrainRendererService")); + } + + void TerrainWorldRendererComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& services) + { + services.push_back(AZ_CRC_CE("TerrainService")); + } + + void TerrainWorldRendererComponent::Reflect(AZ::ReflectContext* context) + { + TerrainWorldRendererConfig::Reflect(context); + + AZ::SerializeContext* serialize = azrtti_cast(context); + if (serialize) + { + serialize->Class()->Version(0)->Field( + "Configuration", &TerrainWorldRendererComponent::m_configuration); + } + } + + TerrainWorldRendererComponent::TerrainWorldRendererComponent(const TerrainWorldRendererConfig& configuration) + : m_configuration(configuration) + { + } + + TerrainWorldRendererComponent::~TerrainWorldRendererComponent() + { + if (m_terrainRendererActive) + { + Deactivate(); + } + } + + AZ::RPI::Scene* TerrainWorldRendererComponent::GetScene() const + { + // Find the entity context for the entity ID. + AzFramework::EntityContextId entityContextId = AzFramework::EntityContextId::CreateNull(); + AzFramework::EntityIdContextQueryBus::EventResult( + entityContextId, GetEntityId(), &AzFramework::EntityIdContextQueryBus::Events::GetOwningContextId); + + return AZ::RPI::Scene::GetSceneForEntityContextId(entityContextId); + } + + void TerrainWorldRendererComponent::Activate() + { + // On component activation, register the terrain feature processor with Atom and the scene related to this entity context. + + AZ::RPI::FeatureProcessorFactory::Get()->RegisterFeatureProcessor(); + + if (AZ::RPI::Scene* scene = GetScene(); scene) + { + m_terrainFeatureProcessor = scene->EnableFeatureProcessor(); + } + + AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusConnect(); + m_terrainRendererActive = true; + } + + void TerrainWorldRendererComponent::Deactivate() + { + // On component deactivation, unregister the feature processor and remove it from the default scene. + + m_terrainRendererActive = false; + AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusDisconnect(); + + if (AZ::RPI::Scene* scene = GetScene(); scene) + { + if (scene->GetFeatureProcessor()) + { + scene->DisableFeatureProcessor(); + } + } + m_terrainFeatureProcessor = nullptr; + + AZ::RPI::FeatureProcessorFactory::Get()->UnregisterFeatureProcessor(); + } + + bool TerrainWorldRendererComponent::ReadInConfig(const AZ::ComponentConfig* baseConfig) + { + if (auto config = azrtti_cast(baseConfig)) + { + m_configuration = *config; + return true; + } + return false; + } + + bool TerrainWorldRendererComponent::WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const + { + if (auto config = azrtti_cast(outBaseConfig)) + { + *config = m_configuration; + return true; + } + return false; + } + + void TerrainWorldRendererComponent::OnTerrainDataDestroyBegin() + { + // If the terrain is being destroyed, remove all existing terrain data from the feature processor. + + if (m_terrainFeatureProcessor) + { + m_terrainFeatureProcessor->RemoveTerrainData(); + } + } + + void TerrainWorldRendererComponent::OnTerrainDataChanged([[maybe_unused]] const AZ::Aabb& dirtyRegion, [[maybe_unused]] TerrainDataChangedMask dataChangedMask) + { + // Block other threads from accessing the surface data bus while we are in GetValue (which may call into the SurfaceData bus). + // We lock our surface data mutex *before* checking / setting "isRequestInProgress" so that we prevent race conditions + // that create false detection of cyclic dependencies when multiple requests occur on different threads simultaneously. + // (One case where this was previously able to occur was in rapid updating of the Preview widget on the + // GradientSurfaceDataComponent in the Editor when moving the threshold sliders back and forth rapidly) + auto& surfaceDataContext = SurfaceData::SurfaceDataSystemRequestBus::GetOrCreateContext(false); + typename SurfaceData::SurfaceDataSystemRequestBus::Context::DispatchLockGuard scopeLock(surfaceDataContext.m_contextMutex); + + AZ::Vector2 queryResolution = AZ::Vector2(1.0f); + AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult( + queryResolution, &AzFramework::Terrain::TerrainDataRequests::GetTerrainHeightQueryResolution); + + AZ::Aabb worldBounds = AZ::Aabb::CreateNull(); + AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult( + worldBounds, &AzFramework::Terrain::TerrainDataRequests::GetTerrainAabb); + + + AZ::Transform transform = AZ::Transform::CreateTranslation(worldBounds.GetCenter()); + + uint32_t width = aznumeric_cast( + (float)worldBounds.GetXExtent() / queryResolution.GetX()); + uint32_t height = aznumeric_cast( + (float)worldBounds.GetYExtent() / queryResolution.GetY()); + AZStd::vector pixels; + pixels.resize_no_construct(width * height); + const uint32_t pixelDataSize = width * height * sizeof(float); + memset(pixels.data(), 0, pixelDataSize); + + for (uint32_t y = 0; y < height; y++) + { + for (uint32_t x = 0; x < width; x++) + { + bool terrainExists = true; + float terrainHeight = 0.0f; + AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult( + terrainHeight, &AzFramework::Terrain::TerrainDataRequests::GetHeightFromFloats, + (x * queryResolution.GetX()) + worldBounds.GetMin().GetX(), + (y * queryResolution.GetY()) + worldBounds.GetMin().GetY(), + AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, + &terrainExists); + + pixels[(y * width) + x] = + (terrainHeight - worldBounds.GetMin().GetZ()) / worldBounds.GetExtents().GetZ(); + } + } + + if (m_terrainFeatureProcessor) + { + m_terrainFeatureProcessor->UpdateTerrainData(transform, worldBounds, queryResolution.GetX(), width, height, pixels); + } + } + +} diff --git a/Gems/Terrain/Code/Source/Components/TerrainWorldRendererComponent.h b/Gems/Terrain/Code/Source/Components/TerrainWorldRendererComponent.h new file mode 100644 index 0000000000..a1e9498be8 --- /dev/null +++ b/Gems/Terrain/Code/Source/Components/TerrainWorldRendererComponent.h @@ -0,0 +1,75 @@ +/* + * 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 + +namespace LmbrCentral +{ + template + class EditorWrappedComponentBase; +} + +namespace AZ::RPI +{ + class Scene; +} + +namespace Terrain +{ + class TerrainFeatureProcessor; + + class TerrainWorldRendererConfig + : public AZ::ComponentConfig + { + public: + AZ_CLASS_ALLOCATOR(TerrainWorldRendererConfig, AZ::SystemAllocator, 0); + AZ_RTTI(TerrainWorldRendererConfig, "{08C5863C-092D-4A69-8226-4978E4F6E343}", AZ::ComponentConfig); + static void Reflect(AZ::ReflectContext* context); + }; + + + class TerrainWorldRendererComponent + : public AZ::Component + , public AzFramework::Terrain::TerrainDataNotificationBus::Handler + { + public: + template + friend class LmbrCentral::EditorWrappedComponentBase; + AZ_COMPONENT(TerrainWorldRendererComponent, "{3B0DB71E-5944-437C-8C88-70F8B405BFC7}"); + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& services); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& services); + static void Reflect(AZ::ReflectContext* context); + + TerrainWorldRendererComponent(const TerrainWorldRendererConfig& configuration); + TerrainWorldRendererComponent() = default; + ~TerrainWorldRendererComponent() override; + + ////////////////////////////////////////////////////////////////////////// + // AZ::Component interface implementation + void Activate() override; + void Deactivate() override; + bool ReadInConfig(const AZ::ComponentConfig* baseConfig) override; + bool WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const override; + + protected: + void OnTerrainDataDestroyBegin() override; + void OnTerrainDataChanged(const AZ::Aabb& dirtyRegion, TerrainDataChangedMask dataChangedMask) override; + + AZ::RPI::Scene* GetScene() const; + + private: + TerrainWorldRendererConfig m_configuration; + bool m_terrainRendererActive{ false }; + TerrainFeatureProcessor* m_terrainFeatureProcessor{ nullptr }; + }; +} diff --git a/Gems/Terrain/Code/Source/EditorComponents/EditorTerrainWorldRendererComponent.cpp b/Gems/Terrain/Code/Source/EditorComponents/EditorTerrainWorldRendererComponent.cpp new file mode 100644 index 0000000000..2d42585fa5 --- /dev/null +++ b/Gems/Terrain/Code/Source/EditorComponents/EditorTerrainWorldRendererComponent.cpp @@ -0,0 +1,56 @@ +/* + * 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 Terrain +{ + void EditorTerrainWorldRendererComponent::Reflect(AZ::ReflectContext* context) + { + BaseClassType::Reflect(context); + + AZ::SerializeContext* serializeContext = azrtti_cast(context); + + if (serializeContext) + { + serializeContext->Class() + ->Version(0) + ; + + if (auto editContext = serializeContext->GetEditContext()) + { + editContext->Class( + "Terrain World Renderer", "") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::Category, "Terrain") + ->Attribute(AZ::Edit::Attributes::Icon, "Editor/Icons/Components/TerrainWorldRenderer.svg") + ->Attribute(AZ::Edit::Attributes::ViewportIcon, "Editor/Icons/Components/Viewport/TerrainWorldRenderer.svg") + ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZStd::vector({ AZ_CRC_CE("Level") })) + ; + } + } + } + + + void EditorTerrainWorldRendererComponent::Init() + { + BaseClassType::Init(); + } + + void EditorTerrainWorldRendererComponent::Activate() + { + BaseClassType::Activate(); + } + + AZ::u32 EditorTerrainWorldRendererComponent::ConfigurationChanged() + { + return BaseClassType::ConfigurationChanged(); + } +} diff --git a/Gems/Terrain/Code/Source/EditorComponents/EditorTerrainWorldRendererComponent.h b/Gems/Terrain/Code/Source/EditorComponents/EditorTerrainWorldRendererComponent.h new file mode 100644 index 0000000000..92ce648988 --- /dev/null +++ b/Gems/Terrain/Code/Source/EditorComponents/EditorTerrainWorldRendererComponent.h @@ -0,0 +1,38 @@ +/* + * 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 + +namespace Terrain +{ + class EditorTerrainWorldRendererComponent + : public LmbrCentral::EditorWrappedComponentBase + { + public: + using BaseClassType = LmbrCentral::EditorWrappedComponentBase; + AZ_EDITOR_COMPONENT(EditorTerrainWorldRendererComponent, "{7BEFF763-89A6-4EDA-B199-B049A8E757AF}", BaseClassType); + static void Reflect(AZ::ReflectContext* context); + + ////////////////////////////////////////////////////////////////////////// + // AZ::Component interface implementation + void Init() override; + void Activate() override; + AZ::u32 ConfigurationChanged() override; + + protected: + using BaseClassType::m_configuration; + using BaseClassType::m_component; + using BaseClassType::m_visible; + + private: + }; +} diff --git a/Gems/Terrain/Code/Source/EditorTerrainModule.cpp b/Gems/Terrain/Code/Source/EditorTerrainModule.cpp index 0ba416cbb7..5305087694 100644 --- a/Gems/Terrain/Code/Source/EditorTerrainModule.cpp +++ b/Gems/Terrain/Code/Source/EditorTerrainModule.cpp @@ -12,6 +12,7 @@ #include #include #include +#include namespace Terrain { @@ -25,6 +26,7 @@ namespace Terrain Terrain::EditorTerrainSystemComponent::CreateDescriptor(), Terrain::EditorTerrainWorldComponent::CreateDescriptor(), Terrain::EditorTerrainWorldDebuggerComponent::CreateDescriptor(), + Terrain::EditorTerrainWorldRendererComponent::CreateDescriptor(), }); } diff --git a/Gems/Terrain/Code/Source/TerrainModule.cpp b/Gems/Terrain/Code/Source/TerrainModule.cpp index 29b2542472..24b87b4ab3 100644 --- a/Gems/Terrain/Code/Source/TerrainModule.cpp +++ b/Gems/Terrain/Code/Source/TerrainModule.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -26,6 +27,7 @@ namespace Terrain TerrainSystemComponent::CreateDescriptor(), TerrainWorldComponent::CreateDescriptor(), TerrainWorldDebuggerComponent::CreateDescriptor(), + TerrainWorldRendererComponent::CreateDescriptor(), TerrainHeightGradientListComponent::CreateDescriptor(), TerrainLayerSpawnerComponent::CreateDescriptor(), TerrainSurfaceDataSystemComponent::CreateDescriptor(), diff --git a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp index 5ec6af6f37..46ac677dd1 100644 --- a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp +++ b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp @@ -12,10 +12,6 @@ #include #include -#include -#include -#include - using namespace Terrain; bool TerrainLayerPriorityComparator::operator()(const AZ::EntityId& layer1id, const AZ::EntityId& layer2id) const @@ -114,18 +110,6 @@ void TerrainSystem::Deactivate() m_terrainSettingsDirty = true; m_requestedSettings.m_systemActive = false; - if (auto rpi = AZ::RPI::RPISystemInterface::Get(); rpi) - { - if (auto defaultScene = rpi->GetDefaultScene(); defaultScene) - { - const AZ::RPI::Scene* scene = defaultScene.get(); - if (auto terrainFeatureProcessor = scene->GetFeatureProcessor(); terrainFeatureProcessor) - { - terrainFeatureProcessor->RemoveTerrainData(); - } - } - } - AzFramework::Terrain::TerrainDataNotificationBus::Broadcast( &AzFramework::Terrain::TerrainDataNotificationBus::Events::OnTerrainDataDestroyEnd); } @@ -507,60 +491,6 @@ void TerrainSystem::OnTick(float /*deltaTime*/, AZ::ScriptTimePoint /*time*/) m_currentSettings = m_requestedSettings; } - if (m_currentSettings.m_systemActive && m_terrainHeightDirty) - { - AZStd::shared_lock lock(m_areaMutex); - - // Block other threads from accessing the surface data bus while we are in GetValue (which may call into the SurfaceData bus). - // We lock our surface data mutex *before* checking / setting "isRequestInProgress" so that we prevent race conditions - // that create false detection of cyclic dependencies when multiple requests occur on different threads simultaneously. - // (One case where this was previously able to occur was in rapid updating of the Preview widget on the - // GradientSurfaceDataComponent in the Editor when moving the threshold sliders back and forth rapidly) - auto& surfaceDataContext = SurfaceData::SurfaceDataSystemRequestBus::GetOrCreateContext(false); - typename SurfaceData::SurfaceDataSystemRequestBus::Context::DispatchLockGuard scopeLock(surfaceDataContext.m_contextMutex); - - AZ::Transform transform = AZ::Transform::CreateTranslation(m_currentSettings.m_worldBounds.GetCenter()); - - uint32_t width = aznumeric_cast( - (float)m_currentSettings.m_worldBounds.GetXExtent() / m_currentSettings.m_heightQueryResolution.GetX()); - uint32_t height = aznumeric_cast( - (float)m_currentSettings.m_worldBounds.GetYExtent() / m_currentSettings.m_heightQueryResolution.GetY()); - AZStd::vector pixels; - pixels.resize_no_construct(width * height); - const uint32_t pixelDataSize = width * height * sizeof(float); - memset(pixels.data(), 0, pixelDataSize); - - for (uint32_t y = 0; y < height; y++) - { - for (uint32_t x = 0; x < width; x++) - { - bool terrainExists; - float terrainHeight = GetTerrainAreaHeight( - (x * m_currentSettings.m_heightQueryResolution.GetX()) + m_currentSettings.m_worldBounds.GetMin().GetX(), - (y * m_currentSettings.m_heightQueryResolution.GetY()) + m_currentSettings.m_worldBounds.GetMin().GetY(), - terrainExists); - - pixels[(y * width) + x] = - (terrainHeight - m_currentSettings.m_worldBounds.GetMin().GetZ()) / - m_currentSettings.m_worldBounds.GetExtents().GetZ(); - } - } - - if (auto rpi = AZ::RPI::RPISystemInterface::Get(); rpi) - { - if (auto defaultScene = rpi->GetDefaultScene(); defaultScene) - { - const AZ::RPI::Scene* scene = defaultScene.get(); - if (auto terrainFeatureProcessor = scene->GetFeatureProcessor(); terrainFeatureProcessor) - { - terrainFeatureProcessor->UpdateTerrainData( - transform, m_currentSettings.m_worldBounds, m_currentSettings.m_heightQueryResolution.GetX(), width, height, - pixels); - } - } - } - } - if (terrainSettingsChanged || m_terrainHeightDirty) { // Block other threads from accessing the surface data bus while we are in GetValue (which may call into the SurfaceData bus). diff --git a/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp b/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp index 91a9744f0b..e4fb0f348e 100644 --- a/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp +++ b/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp @@ -19,15 +19,31 @@ #include using ::testing::AtLeast; +using ::testing::FloatNear; +using ::testing::FloatEq; +using ::testing::IsFalse; +using ::testing::Ne; using ::testing::NiceMock; using ::testing::Return; class TerrainSystemTest : public ::testing::Test { protected: + // Defines a structure for defining both an XY position and the expected height for that position. + struct HeightTestPoint + { + AZ::Vector2 m_testLocation; + float m_expectedHeight; + }; + AZ::ComponentApplication m_app; AZStd::unique_ptr m_terrainSystem; + AZStd::unique_ptr> m_boxShapeRequests; + AZStd::unique_ptr> m_shapeRequests; + AZStd::unique_ptr> m_terrainAreaHeightRequests; + + void SetUp() override { AZ::ComponentApplication::Descriptor appDesc; @@ -41,6 +57,9 @@ protected: void TearDown() override { m_terrainSystem.reset(); + m_boxShapeRequests.reset(); + m_shapeRequests.reset(); + m_terrainAreaHeightRequests.reset(); m_app.Destroy(); } @@ -71,6 +90,53 @@ protected: m_app.RegisterComponentDescriptor(Component::CreateDescriptor()); return entity->CreateComponent(); } + + // Create a terrain system with reasonable defaults for testing, but with the ability to override the defaults + // on a test-by-test basis. + void CreateAndActivateTerrainSystem( + AZ::Vector2 queryResolution = AZ::Vector2(1.0f), + AZ::Aabb worldBounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-128.0f), AZ::Vector3(128.0f))) + { + // Create the terrain system and give it one tick to fully initialize itself. + m_terrainSystem = AZStd::make_unique(); + m_terrainSystem->SetTerrainAabb(worldBounds); + m_terrainSystem->SetTerrainHeightQueryResolution(queryResolution); + m_terrainSystem->Activate(); + AZ::TickBus::Broadcast(&AZ::TickBus::Events::OnTick, 0.f, AZ::ScriptTimePoint{}); + } + + + AZStd::unique_ptr CreateAndActivateMockTerrainLayerSpawner( + const AZ::Aabb& spawnerBox, + const AZStd::function& mockHeights) + { + // Create the base entity with a mock box shape, Terrain Layer Spawner, and height provider. + auto entity = CreateEntity(); + CreateComponent(entity.get()); + CreateComponent(entity.get()); + + m_boxShapeRequests = AZStd::make_unique>(entity->GetId()); + m_shapeRequests = AZStd::make_unique>(entity->GetId()); + + // Set up the box shape to return whatever spawnerBox was passed in. + ON_CALL(*m_shapeRequests, GetEncompassingAabb).WillByDefault(Return(spawnerBox)); + + // Set up a mock height provider to use the passed-in mock height function to generate a height. + m_terrainAreaHeightRequests = AZStd::make_unique>(entity->GetId()); + ON_CALL(*m_terrainAreaHeightRequests, GetHeight) + .WillByDefault( + [mockHeights](const AZ::Vector3& inPosition, AZ::Vector3& outPosition, bool& terrainExists) + { + // By default, set the outPosition to the input position and terrain to always exist. + outPosition = inPosition; + terrainExists = true; + // Let the test function modify these values based on the needs of the specific test. + mockHeights(outPosition, terrainExists); + }); + + ActivateEntity(entity.get()); + return entity; + } }; TEST_F(TerrainSystemTest, TrivialCreateDestroy) @@ -122,10 +188,8 @@ TEST_F(TerrainSystemTest, TerrainDoesNotExistWhenNoTerrainLayerSpawnersAreRegist // will return false for terrainExists, returns a height equal to the min world bounds of the terrain system, and returns // a normal facing up the Z axis. - // Create the terrain system and give it one tick to fully initialize itself. - m_terrainSystem = AZStd::make_unique(); - m_terrainSystem->Activate(); - AZ::TickBus::Broadcast(&AZ::TickBus::Events::OnTick, 0.f, AZ::ScriptTimePoint{}); + // Create and activate the terrain system with our testing defaults for world bounds and query resolution. + CreateAndActivateTerrainSystem(); AZ::Aabb worldBounds = m_terrainSystem->GetTerrainAabb(); @@ -139,7 +203,7 @@ TEST_F(TerrainSystemTest, TerrainDoesNotExistWhenNoTerrainLayerSpawnersAreRegist bool terrainExists = true; float height = m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists); EXPECT_FALSE(terrainExists); - EXPECT_EQ(height, worldBounds.GetMin().GetZ()); + EXPECT_FLOAT_EQ(height, worldBounds.GetMin().GetZ()); terrainExists = true; AZ::Vector3 normal = m_terrainSystem->GetNormal( @@ -162,37 +226,21 @@ TEST_F(TerrainSystemTest, TerrainExistsOnlyWithinTerrainLayerSpawnerBounds) // The terrain system should only query Heights from the TerrainAreaHeightRequest bus within the // TerrainLayerSpawner region, and so those values should only get returned from GetHeight for queries inside that region. - // Create the base entity with a mock Box Shape and a Terrain Layer Spawner. - auto entity = CreateEntity(); - CreateComponent(entity.get()); - CreateComponent(entity.get()); - - // Set up the box shape to return a box from (0,0,5) to (10, 10, 15) - AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 5.0f, 10.0f, 10.0f, 15.0f); - NiceMock boxShapeRequests(entity->GetId()); - NiceMock shapeRequests(entity->GetId()); - ON_CALL(shapeRequests, GetEncompassingAabb).WillByDefault(Return(spawnerBox)); - - // Set up a mock height provider that always returns 5.0 and a normal of Y-up. - const float spawnerHeight = 5.0f; - NiceMock terrainAreaHeightRequests(entity->GetId()); - ON_CALL(terrainAreaHeightRequests, GetHeight) - .WillByDefault( - [spawnerHeight](const AZ::Vector3& inPosition, AZ::Vector3& outPosition, bool& terrainExists) - { - outPosition = inPosition; - outPosition.SetZ(spawnerHeight); - terrainExists = true; - }); - - ActivateEntity(entity.get()); + // Create a mock terrain layer spawner that uses a box of (0,0,5) - (10,10,15) and always returns a height of 5. + constexpr float spawnerHeight = 5.0f; + const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 5.0f, 10.0f, 10.0f, 15.0f); + auto entity = CreateAndActivateMockTerrainLayerSpawner( + spawnerBox, + [](AZ::Vector3& position, bool& terrainExists) + { + position.SetZ(spawnerHeight); + terrainExists = true; + }); // Verify that terrain exists within the layer spawner bounds, and doesn't exist outside of it. - // Create the terrain system and give it one tick to fully initialize itself. - m_terrainSystem = AZStd::make_unique(); - m_terrainSystem->Activate(); - AZ::TickBus::Broadcast(&AZ::TickBus::Events::OnTick, 0.f, AZ::ScriptTimePoint{}); + // Create and activate the terrain system with our testing defaults for world bounds and query resolution. + CreateAndActivateTerrainSystem(); // Create a box that's twice as big as the layer spawner box. Loop through it and verify that points within the layer box contain // terrain and the expected height & normal values, and points outside the layer box don't contain terrain. @@ -215,7 +263,7 @@ TEST_F(TerrainSystemTest, TerrainExistsOnlyWithinTerrainLayerSpawnerBounds) { EXPECT_TRUE(heightQueryTerrainExists); EXPECT_FALSE(isHole); - EXPECT_EQ(height, spawnerHeight); + EXPECT_FLOAT_EQ(height, spawnerHeight); } else { @@ -226,3 +274,202 @@ TEST_F(TerrainSystemTest, TerrainExistsOnlyWithinTerrainLayerSpawnerBounds) } } +TEST_F(TerrainSystemTest, TerrainHeightQueriesWithExactSamplersIgnoreQueryGrid) +{ + // Verify that when using the "EXACT" height sampler, the returned heights come directly from the height provider at the exact + // requested location, instead of the position being quantized to the height query grid. + + // Create a mock terrain layer spawner that uses a box of (0,0,5) - (10,10,15) and generates a height based on a sine wave + // using a frequency of 1m and an amplitude of 10m. i.e. Heights will range between -10 to 10 meters, but will have a value of 0 + // every 0.5 meters. The sine wave value is based on the absolute X position only, for simplicity. + constexpr float amplitudeMeters = 10.0f; + constexpr float frequencyMeters = 1.0f; + const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 5.0f, 10.0f, 10.0f, 15.0f); + auto entity = CreateAndActivateMockTerrainLayerSpawner( + spawnerBox, + [](AZ::Vector3& position, bool& terrainExists) + { + position.SetZ(amplitudeMeters * sin(AZ::Constants::TwoPi * (position.GetX() / frequencyMeters))); + terrainExists = true; + }); + + // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution that exactly matches + // the frequency of our sine wave. If our height queries rely on the query resolution, we should always get a value of 0. + const AZ::Vector2 queryResolution(frequencyMeters); + CreateAndActivateTerrainSystem(queryResolution); + + // Test an arbitrary set of points that should all produce non-zero heights with the EXACT sampler. They're not aligned with the + // query resolution, or with the 0 points on the sine wave. + const AZ::Vector2 nonZeroPoints[] = { AZ::Vector2(0.3f), AZ::Vector2(2.8f), AZ::Vector2(5.9f), AZ::Vector2(7.7f) }; + for (auto& nonZeroPoint : nonZeroPoints) + { + AZ::Vector3 position(nonZeroPoint.GetX(), nonZeroPoint.GetY(), 0.0f); + bool heightQueryTerrainExists = false; + float height = + m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &heightQueryTerrainExists); + + // We've chosen a bunch of places on the sine wave that should return a non-zero positive or negative value. + constexpr float epsilon = 0.0001f; + EXPECT_GT(fabsf(height), epsilon); + } + + // Test an arbitrary set of points that should all produce zero heights with the EXACT sampler, since they align with 0 points on the + // sine wave, regardless of whether or not they align to the query resolution. + const AZ::Vector2 zeroPoints[] = { AZ::Vector2(0.5f), AZ::Vector2(1.0f), AZ::Vector2(5.0f), AZ::Vector2(7.5f) }; + for (auto& zeroPoint : zeroPoints) + { + AZ::Vector3 position(zeroPoint.GetX(), zeroPoint.GetY(), 0.0f); + bool heightQueryTerrainExists = false; + float height = + m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &heightQueryTerrainExists); + + constexpr float epsilon = 0.0001f; + EXPECT_NEAR(height, 0.0f, epsilon); + } +} + +TEST_F(TerrainSystemTest, TerrainHeightQueriesWithClampSamplersUseQueryGrid) +{ + // Verify that when using the "CLAMP" height sampler, the requested location is quantized to the height query grid before fetching + // the height. + + // Create a mock terrain layer spawner that uses a box of (-10,-10,-5) - (10,10,15) and generates a height equal + // to the X + Y position, so if either one doesn't get clamped we'll get an unexpected result. + const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f); + auto entity = CreateAndActivateMockTerrainLayerSpawner( + spawnerBox, + [](AZ::Vector3& position, bool& terrainExists) + { + position.SetZ(position.GetX() + position.GetY()); + terrainExists = true; + }); + + // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 0.25 meter intervals. + const AZ::Vector2 queryResolution(0.25f); + CreateAndActivateTerrainSystem(queryResolution); + + // Test some points and verify that the results always go "downward", whether they're in positive or negative space. + // (Z contains the the expected result for convenience). + const HeightTestPoint testPoints[] = + { + { AZ::Vector2(0.0f, 0.0f), 0.0f }, // Should return a height of 0.00 + 0.00 + { AZ::Vector2(0.3f, 0.3f), 0.5f }, // Should return a height of 0.25 + 0.25 + { AZ::Vector2(2.8f, 2.8f), 5.5f }, // Should return a height of 2.75 + 2.75 + { AZ::Vector2(5.5f, 5.5f), 11.0f }, // Should return a height of 5.50 + 5.50 + { AZ::Vector2(7.7f, 7.7f), 15.0f }, // Should return a height of 7.50 + 7.50 + + { AZ::Vector2(-0.3f, -0.3f), -1.0f }, // Should return a height of -0.50 + -0.50 + { AZ::Vector2(-2.8f, -2.8f), -6.0f }, // Should return a height of -3.00 + -3.00 + { AZ::Vector2(-5.5f, -5.5f), -11.0f }, // Should return a height of -5.50 + -5.50 + { AZ::Vector2(-7.7f, -7.7f), -15.5f } // Should return a height of -7.75 + -7.75 + }; + for (auto& testPoint : testPoints) + { + const float expectedHeight = testPoint.m_expectedHeight; + + AZ::Vector3 position(testPoint.m_testLocation.GetX(), testPoint.m_testLocation.GetY(), 0.0f); + bool heightQueryTerrainExists = false; + float height = + m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP, &heightQueryTerrainExists); + + constexpr float epsilon = 0.0001f; + EXPECT_NEAR(height, expectedHeight, epsilon); + } +} + +TEST_F(TerrainSystemTest, TerrainHeightQueriesWithBilinearSamplersUseQueryGridToInterpolate) +{ + // Verify that when using the "BILINEAR" height sampler, the heights are interpolated from points sampled from the query grid. + + // Create a mock terrain layer spawner that uses a box of (-10,-10,-5) - (10,10,15) and generates a height equal + // to the X + Y position, so we'll have heights that look like this on our grid: + // 0 *---* 1 + // | | + // 1 *---* 2 + // However, everywhere inside the grid box, we'll generate heights much larger than X + Y. It will have no effect on exact grid + // points, but it will noticeably affect the expected height values if any points get sampled in-between grid points. + + const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f); + const float amplitudeMeters = 10.0f; + const float frequencyMeters = 1.0f; + auto entity = CreateAndActivateMockTerrainLayerSpawner( + spawnerBox, + [amplitudeMeters, frequencyMeters](AZ::Vector3& position, bool& terrainExists) + { + // Our generated height will be X + Y. + float expectedHeight = position.GetX() + position.GetY(); + + // If either X or Y aren't evenly divisible by the query frequency, add a scaled value to our generated height. + // This will show up as an unexpected height "spike" if it gets used in any bilinear filter queries. + float unexpectedVariance = amplitudeMeters * + (fmodf(position.GetX(), frequencyMeters) + fmodf(position.GetY(), frequencyMeters)); + position.SetZ(expectedHeight + unexpectedVariance); + terrainExists = true; + }); + + // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 1 meter intervals. + const AZ::Vector2 queryResolution(frequencyMeters); + CreateAndActivateTerrainSystem(queryResolution); + + // Test some points and verify that the results are the expected bilinear filtered result, + // whether they're in positive or negative space. + // (Z contains the the expected result for convenience). + const HeightTestPoint testPoints[] = { + + // Queries directly on grid points. These should return values of X + Y. + { AZ::Vector2(0.0f, 0.0f), 0.0f }, // Should return a height of 0 + 0 + { AZ::Vector2(1.0f, 0.0f), 1.0f }, // Should return a height of 1 + 0 + { AZ::Vector2(0.0f, 1.0f), 1.0f }, // Should return a height of 0 + 1 + { AZ::Vector2(1.0f, 1.0f), 2.0f }, // Should return a height of 1 + 1 + { AZ::Vector2(3.0f, 5.0f), 8.0f }, // Should return a height of 3 + 5 + + { AZ::Vector2(-1.0f, 0.0f), -1.0f }, // Should return a height of -1 + 0 + { AZ::Vector2(0.0f, -1.0f), -1.0f }, // Should return a height of 0 + -1 + { AZ::Vector2(-1.0f, -1.0f), -2.0f }, // Should return a height of -1 + -1 + { AZ::Vector2(-3.0f, -5.0f), -8.0f }, // Should return a height of -3 + -5 + + // Queries that are on a grid edge (one axis on the grid, the other somewhere in-between). + // These should just be a linear interpolation of the points, so it should still be X + Y. + + { AZ::Vector2(0.25f, 0.0f), 0.25f }, // Should return a height of -0.25 + 0 + { AZ::Vector2(3.75f, 0.0f), 3.75f }, // Should return a height of -3.75 + 0 + { AZ::Vector2(0.0f, 0.25f), 0.25f }, // Should return a height of 0 + -0.25 + { AZ::Vector2(0.0f, 3.75f), 3.75f }, // Should return a height of 0 + -3.75 + + { AZ::Vector2(2.0f, 3.75f), 5.75f }, // Should return a height of -2 + -3.75 + { AZ::Vector2(2.25f, 4.0f), 6.25f }, // Should return a height of -2.25 + -4 + + { AZ::Vector2(-0.25f, 0.0f), -0.25f }, // Should return a height of -0.25 + 0 + { AZ::Vector2(-3.75f, 0.0f), -3.75f }, // Should return a height of -3.75 + 0 + { AZ::Vector2(0.0f, -0.25f), -0.25f }, // Should return a height of 0 + -0.25 + { AZ::Vector2(0.0f, -3.75f), -3.75f }, // Should return a height of 0 + -3.75 + + { AZ::Vector2(-2.0f, -3.75f), -5.75f }, // Should return a height of -2 + -3.75 + { AZ::Vector2(-2.25f, -4.0f), -6.25f }, // Should return a height of -2.25 + -4 + + // Queries inside a grid square (both axes are in-between grid points) + // This is a full bilinear interpolation, but because we're using X + Y for our heights, the interpolated values + // should *still* be X + Y assuming the points were sampled correctly from the grid points. + + { AZ::Vector2(3.25f, 5.25f), 8.5f }, // Should return a height of 3.25 + 5.25 + { AZ::Vector2(7.71f, 9.74f), 17.45f }, // Should return a height of 7.71 + 9.74 + + { AZ::Vector2(-3.25f, -5.25f), -8.5f }, // Should return a height of -3.25 + -5.25 + { AZ::Vector2(-7.71f, -9.74f), -17.45f }, // Should return a height of -7.71 + -9.74 + }; + + // Loop through every test point and validate it. + for (auto& testPoint : testPoints) + { + const float expectedHeight = testPoint.m_expectedHeight; + + AZ::Vector3 position(testPoint.m_testLocation.GetX(), testPoint.m_testLocation.GetY(), 0.0f); + bool heightQueryTerrainExists = false; + float height = + m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR, &heightQueryTerrainExists); + + // Verify that our height query returned the bilinear filtered result we expect. + constexpr float epsilon = 0.0001f; + EXPECT_NEAR(height, expectedHeight, epsilon); + } +} diff --git a/Gems/Terrain/Code/terrain_editor_shared_files.cmake b/Gems/Terrain/Code/terrain_editor_shared_files.cmake index 9868f7d310..d4719f46d5 100644 --- a/Gems/Terrain/Code/terrain_editor_shared_files.cmake +++ b/Gems/Terrain/Code/terrain_editor_shared_files.cmake @@ -15,6 +15,8 @@ set(FILES Source/EditorComponents/EditorTerrainWorldComponent.h Source/EditorComponents/EditorTerrainWorldDebuggerComponent.cpp Source/EditorComponents/EditorTerrainWorldDebuggerComponent.h + Source/EditorComponents/EditorTerrainWorldRendererComponent.cpp + Source/EditorComponents/EditorTerrainWorldRendererComponent.h Source/EditorComponents/EditorTerrainSystemComponent.cpp Source/EditorComponents/EditorTerrainSystemComponent.h Source/EditorTerrainModule.cpp diff --git a/Gems/Terrain/Code/terrain_files.cmake b/Gems/Terrain/Code/terrain_files.cmake index 6a76dd7141..2c013fa332 100644 --- a/Gems/Terrain/Code/terrain_files.cmake +++ b/Gems/Terrain/Code/terrain_files.cmake @@ -19,6 +19,8 @@ set(FILES Source/Components/TerrainWorldComponent.h Source/Components/TerrainWorldDebuggerComponent.cpp Source/Components/TerrainWorldDebuggerComponent.h + Source/Components/TerrainWorldRendererComponent.cpp + Source/Components/TerrainWorldRendererComponent.h Source/TerrainRenderer/TerrainFeatureProcessor.cpp Source/TerrainRenderer/TerrainFeatureProcessor.h Source/TerrainSystem/TerrainSystem.cpp