diff --git a/Gems/LmbrCentral/Code/CMakeLists.txt b/Gems/LmbrCentral/Code/CMakeLists.txt index 00176ae2c2..b047a9f65e 100644 --- a/Gems/LmbrCentral/Code/CMakeLists.txt +++ b/Gems/LmbrCentral/Code/CMakeLists.txt @@ -114,6 +114,16 @@ endif() # Tests ################################################################################ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) + ly_add_target( + NAME LmbrCentral.Mocks HEADERONLY + NAMESPACE Gem + FILES_CMAKE + lmbrcentral_mocks_files.cmake + INCLUDE_DIRECTORIES + INTERFACE + Mocks + ) + ly_add_target( NAME LmbrCentral.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} NAMESPACE Gem @@ -131,6 +141,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) Legacy::CryCommon AZ::AzFramework Gem::LmbrCentral.Static + Gem::LmbrCentral.Mocks ) ly_add_googletest( NAME Gem::LmbrCentral.Tests diff --git a/Gems/LmbrCentral/Code/Mocks/LmbrCentral/Shape/MockShapes.h b/Gems/LmbrCentral/Code/Mocks/LmbrCentral/Shape/MockShapes.h new file mode 100644 index 0000000000..20be74dd90 --- /dev/null +++ b/Gems/LmbrCentral/Code/Mocks/LmbrCentral/Shape/MockShapes.h @@ -0,0 +1,60 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ +#pragma once + +#include + +#include +#include +#include + +namespace UnitTest +{ + class MockBoxShapeComponentRequests + : public LmbrCentral::BoxShapeComponentRequestsBus::Handler + { + public: + MockBoxShapeComponentRequests(AZ::EntityId entityId) + { + LmbrCentral::BoxShapeComponentRequestsBus::Handler::BusConnect(entityId); + } + + ~MockBoxShapeComponentRequests() + { + LmbrCentral::BoxShapeComponentRequestsBus::Handler::BusDisconnect(); + } + + MOCK_METHOD0(GetBoxConfiguration, LmbrCentral::BoxShapeConfig()); + MOCK_METHOD0(GetBoxDimensions, AZ::Vector3()); + MOCK_METHOD1(SetBoxDimensions, void(const AZ::Vector3& newDimensions)); + }; + + class MockShapeComponentRequests + : public LmbrCentral::ShapeComponentRequestsBus::Handler + { + public: + MockShapeComponentRequests(AZ::EntityId entityId) + { + LmbrCentral::ShapeComponentRequestsBus::Handler::BusConnect(entityId); + } + + ~MockShapeComponentRequests() + { + LmbrCentral::ShapeComponentRequestsBus::Handler::BusDisconnect(); + } + + MOCK_METHOD0(GetShapeType, AZ::Crc32()); + MOCK_METHOD0(GetEncompassingAabb, AZ::Aabb()); + MOCK_METHOD2(GetTransformAndLocalBounds, void(AZ::Transform& transform, AZ::Aabb& bounds)); + MOCK_METHOD1(IsPointInside, bool(const AZ::Vector3& point)); + MOCK_METHOD1(DistanceSquaredFromPoint, float(const AZ::Vector3& point)); + MOCK_METHOD1(GenerateRandomPointInside, AZ::Vector3(AZ::RandomDistributionType randomDistribution)); + MOCK_METHOD3(IntersectRay, bool(const AZ::Vector3& src, const AZ::Vector3& dir, float& distance)); + }; +} + diff --git a/Gems/LmbrCentral/Code/Tests/LmbrCentralTest.cpp b/Gems/LmbrCentral/Code/Tests/LmbrCentralTest.cpp index 40217ff9bc..f36978f22d 100644 --- a/Gems/LmbrCentral/Code/Tests/LmbrCentralTest.cpp +++ b/Gems/LmbrCentral/Code/Tests/LmbrCentralTest.cpp @@ -8,4 +8,7 @@ #include +// Include any public mocks here to ensure they get compiled as a part of the test project. +#include + AZ_UNIT_TEST_HOOK(DEFAULT_UNIT_TEST_ENV); diff --git a/Gems/LmbrCentral/Code/lmbrcentral_mocks_files.cmake b/Gems/LmbrCentral/Code/lmbrcentral_mocks_files.cmake new file mode 100644 index 0000000000..c3a5cca3f8 --- /dev/null +++ b/Gems/LmbrCentral/Code/lmbrcentral_mocks_files.cmake @@ -0,0 +1,11 @@ +# +# 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 +# +# + +set(FILES + Mocks/LmbrCentral/Shape/MockShapes.h +) diff --git a/Gems/Terrain/Code/CMakeLists.txt b/Gems/Terrain/Code/CMakeLists.txt index 0feacdaf71..fc07294dab 100644 --- a/Gems/Terrain/Code/CMakeLists.txt +++ b/Gems/Terrain/Code/CMakeLists.txt @@ -83,6 +83,15 @@ endif() ################################################################################ # See if globally, tests are supported if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) + ly_add_target( + NAME Terrain.Mocks HEADERONLY + NAMESPACE Gem + FILES_CMAKE + terrain_mocks_files.cmake + INCLUDE_DIRECTORIES + INTERFACE + Mocks + ) ly_add_target( NAME Terrain.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} NAMESPACE Gem @@ -97,6 +106,8 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) PRIVATE AZ::AzTest AZ::AzFramework + Gem::LmbrCentral.Mocks + Gem::Terrain.Mocks Gem::Terrain.Static ) @@ -120,6 +131,8 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) BUILD_DEPENDENCIES PRIVATE AZ::AzTest + Gem::LmbrCentral.Mocks + Gem::Terrain.Mocks Gem::Terrain.Editor ) diff --git a/Gems/Terrain/Code/Tests/TerrainMocks.h b/Gems/Terrain/Code/Mocks/Terrain/MockTerrain.h similarity index 57% rename from Gems/Terrain/Code/Tests/TerrainMocks.h rename to Gems/Terrain/Code/Mocks/Terrain/MockTerrain.h index 03a2eccf1d..ea0413be9a 100644 --- a/Gems/Terrain/Code/Tests/TerrainMocks.h +++ b/Gems/Terrain/Code/Mocks/Terrain/MockTerrain.h @@ -11,59 +11,9 @@ #include #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)); - provided.push_back(AZ_CRC_CE("AxisAlignedBoxShapeService")); - } - - 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 MockTerrainSystemService : private Terrain::TerrainSystemServiceRequestBus::Handler { @@ -106,4 +56,40 @@ namespace UnitTest MOCK_METHOD2(OnTerrainDataChanged, void(const AZ::Aabb& dirtyRegion, TerrainDataChangedMask dataChangedMask)); }; + class MockTerrainAreaHeightRequests : public Terrain::TerrainAreaHeightRequestBus::Handler + { + public: + MockTerrainAreaHeightRequests(AZ::EntityId entityId) + { + Terrain::TerrainAreaHeightRequestBus::Handler::BusConnect(entityId); + } + + ~MockTerrainAreaHeightRequests() + { + Terrain::TerrainAreaHeightRequestBus::Handler::BusDisconnect(); + } + + MOCK_METHOD3(GetHeight, void( + const AZ::Vector3& inPosition, + AZ::Vector3& outPosition, + bool& terrainExists)); + + }; + + class MockTerrainSpawnerRequests : public Terrain::TerrainSpawnerRequestBus::Handler + { + public: + MockTerrainSpawnerRequests(AZ::EntityId entityId) + { + Terrain::TerrainSpawnerRequestBus::Handler::BusConnect(entityId); + } + + ~MockTerrainSpawnerRequests() + { + Terrain::TerrainSpawnerRequestBus::Handler::BusDisconnect(); + } + + MOCK_METHOD2(GetPriority, void(AZ::u32& outLayer, AZ::u32& outPriority)); + MOCK_METHOD0(GetUseGroundPlane, bool()); + }; } diff --git a/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.cpp b/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.cpp index 4d0cb6576d..4ea16018d1 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.cpp +++ b/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.cpp @@ -142,11 +142,15 @@ namespace Terrain return false; } - float TerrainHeightGradientListComponent::GetHeight(float x, float y) + void TerrainHeightGradientListComponent::GetHeight( + const AZ::Vector3& inPosition, + AZ::Vector3& outPosition, + bool& terrainExists) { float maxSample = 0.0f; + terrainExists = false; - GradientSignal::GradientSampleParams params(AZ::Vector3(x, y, 0.0f)); + GradientSignal::GradientSampleParams params(AZ::Vector3(inPosition.GetX(), inPosition.GetY(), 0.0f)); // Right now, when the list contains multiple entries, we will use the highest point from each gradient. // This is needed in part because gradients don't really have world bounds, so they exist everywhere but generally have a value @@ -155,49 +159,20 @@ namespace Terrain // make this list a prioritized list from top to bottom for any points that overlap. for (auto& gradientId : m_configuration.m_gradientEntities) { + // If gradients ever provide bounds, or if we add a value threshold in this component, it would be possible for terrain + // to *not* exist at a specific point. + terrainExists = true; + float sample = 0.0f; - GradientSignal::GradientRequestBus::EventResult(sample, gradientId, &GradientSignal::GradientRequestBus::Events::GetValue, params); + GradientSignal::GradientRequestBus::EventResult( + sample, gradientId, &GradientSignal::GradientRequestBus::Events::GetValue, params); maxSample = AZ::GetMax(maxSample, sample); } const float height = AZ::Lerp(m_cachedShapeBounds.GetMin().GetZ(), m_cachedShapeBounds.GetMax().GetZ(), maxSample); - - return AZ::GetClamp(height, m_cachedMinWorldHeight, m_cachedMaxWorldHeight); + outPosition.SetZ(AZ::GetClamp(height, m_cachedMinWorldHeight, m_cachedMaxWorldHeight)); } - void TerrainHeightGradientListComponent::GetHeight( - const AZ::Vector3& inPosition, - AZ::Vector3& outPosition, - [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::Sampler sampleFilter = - AzFramework::Terrain::TerrainDataRequests::Sampler::DEFAULT) - { - const float height = GetHeight(inPosition.GetX(), inPosition.GetY()); - outPosition.SetZ(height); - } - - void TerrainHeightGradientListComponent::GetNormal( - const AZ::Vector3& inPosition, - AZ::Vector3& outNormal, - [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::Sampler sampleFilter = - AzFramework::Terrain::TerrainDataRequests::Sampler::DEFAULT) - { - const float x = inPosition.GetX(); - const float y = inPosition.GetY(); - - if ((x >= m_cachedShapeBounds.GetMin().GetX()) && (x <= m_cachedShapeBounds.GetMax().GetX()) && - (y >= m_cachedShapeBounds.GetMin().GetY()) && (y <= m_cachedShapeBounds.GetMax().GetY())) - { - AZ::Vector2 fRange = (m_cachedHeightQueryResolution / 2.0f) + AZ::Vector2(0.05f); - - AZ::Vector3 v1(x - fRange.GetX(), y - fRange.GetY(), GetHeight(x - fRange.GetX(), y - fRange.GetY())); - AZ::Vector3 v2(x - fRange.GetX(), y + fRange.GetY(), GetHeight(x - fRange.GetX(), y + fRange.GetY())); - AZ::Vector3 v3(x + fRange.GetX(), y - fRange.GetY(), GetHeight(x + fRange.GetX(), y - fRange.GetY())); - AZ::Vector3 v4(x + fRange.GetX(), y + fRange.GetY(), GetHeight(x + fRange.GetX(), y + fRange.GetY())); - outNormal = (v3 - v2).Cross(v4 - v1).GetNormalized(); - } - } - - void TerrainHeightGradientListComponent::OnCompositionChanged() { RefreshMinMaxHeights(); diff --git a/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.h b/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.h index 509f003afa..6c3fd7b820 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.h +++ b/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.h @@ -64,14 +64,7 @@ namespace Terrain TerrainHeightGradientListComponent() = default; ~TerrainHeightGradientListComponent() = default; - void GetHeight( - const AZ::Vector3& inPosition, - AZ::Vector3& outPosition, - AzFramework::Terrain::TerrainDataRequests::Sampler sampleFilter) override; - void GetNormal( - const AZ::Vector3& inPosition, - AZ::Vector3& outNormal, - AzFramework::Terrain::TerrainDataRequests::Sampler sampleFilter) override; + void GetHeight(const AZ::Vector3& inPosition, AZ::Vector3& outPosition, bool& terrainExists) override; ////////////////////////////////////////////////////////////////////////// // AZ::Component interface implementation @@ -91,11 +84,7 @@ namespace Terrain private: TerrainHeightGradientListConfig m_configuration; - /////////////////////////////////////////// - void GetNormalSynchronous(float x, float y, AZ::Vector3& normal); - void RefreshMinMaxHeights(); - float GetHeight(float x, float y); float m_cachedMinWorldHeight{ 0.0f }; float m_cachedMaxWorldHeight{ 0.0f }; diff --git a/Gems/Terrain/Code/Source/Components/TerrainWorldDebuggerComponent.cpp b/Gems/Terrain/Code/Source/Components/TerrainWorldDebuggerComponent.cpp index 89ffa6364f..7233d08281 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainWorldDebuggerComponent.cpp +++ b/Gems/Terrain/Code/Source/Components/TerrainWorldDebuggerComponent.cpp @@ -285,13 +285,13 @@ namespace Terrain AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult( z00, &AzFramework::Terrain::TerrainDataRequests::GetHeightFromFloats, x, y, - AzFramework::Terrain::TerrainDataRequests::Sampler::DEFAULT, &terrainExists); + AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists); AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult( z01, &AzFramework::Terrain::TerrainDataRequests::GetHeightFromFloats, x, y1, - AzFramework::Terrain::TerrainDataRequests::Sampler::DEFAULT, &terrainExists); + AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists); AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult( z10, &AzFramework::Terrain::TerrainDataRequests::GetHeightFromFloats, x1, y, - AzFramework::Terrain::TerrainDataRequests::Sampler::DEFAULT, &terrainExists); + AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists); sector.m_lineVertices.push_back(AZ::Vector3(x, y, z00)); sector.m_lineVertices.push_back(AZ::Vector3(x1, y, z10)); diff --git a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp index 674af376e4..5ec6af6f37 100644 --- a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp +++ b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp @@ -152,27 +152,69 @@ AZ::Vector2 TerrainSystem::GetTerrainHeightQueryResolution() const return m_currentSettings.m_heightQueryResolution; } +void TerrainSystem::ClampPosition(float x, float y, AZ::Vector2& outPosition, AZ::Vector2& normalizedDelta) const +{ + // Given an input position, clamp the values to our terrain grid, where it will always go to the terrain grid point + // at a lower value, whether positive or negative. Ex: 3.3 -> 3, -3.3 -> -4 + // Also, return the normalized delta as a value of [0-1) describing what fraction of a grid point the value moved. + + // Scale the position by the query resolution, so that integer values represent exact steps on the grid, + // and fractional values are the amount in-between each grid point, in the range [0-1). + AZ::Vector2 normalizedPosition = AZ::Vector2(x, y) / m_currentSettings.m_heightQueryResolution; + normalizedDelta = AZ::Vector2( + normalizedPosition.GetX() - floor(normalizedPosition.GetX()), normalizedPosition.GetY() - floor(normalizedPosition.GetY())); + + // Remove the fractional part, then scale back down into world space. + outPosition = (normalizedPosition - normalizedDelta) * m_currentSettings.m_heightQueryResolution; +} + float TerrainSystem::GetHeightSynchronous(float x, float y, Sampler sampler, bool* terrainExistsPtr) const { bool terrainExists = false; - - AZ::Vector3 inPosition((float)x, (float)y, m_currentSettings.m_worldBounds.GetMin().GetZ()); - AZ::Vector3 outPosition((float)x, (float)y, m_currentSettings.m_worldBounds.GetMin().GetZ()); + float height = m_currentSettings.m_worldBounds.GetMin().GetZ(); AZStd::shared_lock lock(m_areaMutex); - for (auto& [areaId, areaBounds] : m_registeredAreas) + switch (sampler) { - inPosition.SetZ(areaBounds.GetMin().GetZ()); - if (areaBounds.Contains(inPosition)) + // Get the value at the requested location, using the terrain grid to bilinear filter between sample grid points. + case AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR: { - Terrain::TerrainAreaHeightRequestBus::Event( - areaId, &Terrain::TerrainAreaHeightRequestBus::Events::GetHeight, inPosition, outPosition, sampler); + // pos0 contains one corner of our grid square, pos1 contains the opposite corner, and normalizedDelta is the fractional + // amount the position exists between those corners. + // Ex: (3.3, 4.4) would have a pos0 of (3, 4), a pos1 of (4, 5), and a delta of (0.3, 0.4). + AZ::Vector2 normalizedDelta; + AZ::Vector2 pos0; + ClampPosition(x, y, pos0, normalizedDelta); + const AZ::Vector2 pos1 = pos0 + m_currentSettings.m_heightQueryResolution; - terrainExists = true; - - break; + const float heightX0Y0 = GetTerrainAreaHeight(pos0.GetX(), pos0.GetY(), terrainExists); + const float heightX1Y0 = GetTerrainAreaHeight(pos1.GetX(), pos0.GetY(), terrainExists); + const float heightX0Y1 = GetTerrainAreaHeight(pos0.GetX(), pos1.GetY(), terrainExists); + const float heightX1Y1 = GetTerrainAreaHeight(pos1.GetX(), pos1.GetY(), terrainExists); + const float heightXY0 = AZ::Lerp(heightX0Y0, heightX1Y0, normalizedDelta.GetX()); + const float heightXY1 = AZ::Lerp(heightX0Y1, heightX1Y1, normalizedDelta.GetX()); + height = AZ::Lerp(heightXY0, heightXY1, normalizedDelta.GetY()); } + break; + + //! Clamp the input point to the terrain sample grid, then get the height at the given grid location. + case AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP: + { + AZ::Vector2 normalizedDelta; + AZ::Vector2 clampedPosition; + ClampPosition(x, y, clampedPosition, normalizedDelta); + + height = GetTerrainAreaHeight(clampedPosition.GetX(), clampedPosition.GetY(), terrainExists); + } + break; + + //! Directly get the value at the location, regardless of terrain sample grid density. + case AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT: + [[fallthrough]]; + default: + height = GetTerrainAreaHeight(x, y, terrainExists); + break; } if (terrainExistsPtr) @@ -181,7 +223,30 @@ float TerrainSystem::GetHeightSynchronous(float x, float y, Sampler sampler, boo } return AZ::GetClamp( - outPosition.GetZ(), m_currentSettings.m_worldBounds.GetMin().GetZ(), m_currentSettings.m_worldBounds.GetMax().GetZ()); + height, m_currentSettings.m_worldBounds.GetMin().GetZ(), m_currentSettings.m_worldBounds.GetMax().GetZ()); +} + +float TerrainSystem::GetTerrainAreaHeight(float x, float y, bool& terrainExists) const +{ + AZ::Vector3 inPosition((float)x, (float)y, m_currentSettings.m_worldBounds.GetMin().GetZ()); + float height = m_currentSettings.m_worldBounds.GetMin().GetZ(); + + AZStd::shared_lock lock(m_areaMutex); + + for (auto& [areaId, areaBounds] : m_registeredAreas) + { + inPosition.SetZ(areaBounds.GetMin().GetZ()); + if (areaBounds.Contains(inPosition)) + { + AZ::Vector3 outPosition; + Terrain::TerrainAreaHeightRequestBus::Event( + areaId, &Terrain::TerrainAreaHeightRequestBus::Events::GetHeight, inPosition, outPosition, terrainExists); + height = outPosition.GetZ(); + break; + } + } + + return height; } float TerrainSystem::GetHeight(AZ::Vector3 position, Sampler sampler, bool* terrainExistsPtr) const @@ -203,24 +268,24 @@ bool TerrainSystem::GetIsHoleFromFloats(float x, float y, Sampler sampler) const AZ::Vector3 TerrainSystem::GetNormalSynchronous(float x, float y, Sampler sampler, bool* terrainExistsPtr) const { - bool terrainExists = false; - - AZ::Vector3 inPosition((float)x, (float)y, m_currentSettings.m_worldBounds.GetMin().GetZ()); - AZ::Vector3 outNormal = AZ::Vector3::CreateAxisZ(); - AZStd::shared_lock lock(m_areaMutex); - for (auto& [areaId, areaBounds] : m_registeredAreas) - { - inPosition.SetZ(areaBounds.GetMin().GetZ()); - if (areaBounds.Contains(inPosition)) - { - Terrain::TerrainAreaHeightRequestBus::Event( - areaId, &Terrain::TerrainAreaHeightRequestBus::Events::GetNormal, inPosition, outNormal, sampler); - terrainExists = true; - break; - } - } + bool terrainExists = false; + + AZ::Vector3 outNormal = AZ::Vector3::CreateAxisZ(); + + const AZ::Vector2 range = (m_currentSettings.m_heightQueryResolution / 2.0f); + const AZ::Vector2 left (x - range.GetX(), y); + const AZ::Vector2 right(x + range.GetX(), y); + const AZ::Vector2 up (x, y - range.GetY()); + const AZ::Vector2 down (x, y + range.GetY()); + + AZ::Vector3 v1(up.GetX(), up.GetY(), GetHeightSynchronous(up.GetX(), up.GetY(), sampler, &terrainExists)); + AZ::Vector3 v2(left.GetX(), left.GetY(), GetHeightSynchronous(left.GetX(), left.GetY(), sampler, &terrainExists)); + AZ::Vector3 v3(right.GetX(), right.GetY(), GetHeightSynchronous(right.GetX(), right.GetY(), sampler, &terrainExists)); + AZ::Vector3 v4(down.GetX(), down.GetY(), GetHeightSynchronous(down.GetX(), down.GetY(), sampler, &terrainExists)); + + outNormal = (v3 - v2).Cross(v4 - v1).GetNormalized(); if (terrainExistsPtr) { @@ -469,43 +534,30 @@ void TerrainSystem::OnTick(float /*deltaTime*/, AZ::ScriptTimePoint /*time*/) { 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()); + 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); - if (!areaBounds.Contains(inPosition)) - { - continue; - } - - AZ::Vector3 outPosition; - const AzFramework::Terrain::TerrainDataRequestBus::Events::Sampler sampleFilter = - AzFramework::Terrain::TerrainDataRequestBus::Events::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; - } + pixels[(y * width) + x] = + (terrainHeight - m_currentSettings.m_worldBounds.GetMin().GetZ()) / + m_currentSettings.m_worldBounds.GetExtents().GetZ(); } } - - const AZ::RPI::Scene* scene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene().get(); - auto terrainFeatureProcessor = scene->GetFeatureProcessor(); - - AZ_Assert(terrainFeatureProcessor, "Unable to find a TerrainFeatureProcessor."); - if (terrainFeatureProcessor) + if (auto rpi = AZ::RPI::RPISystemInterface::Get(); rpi) { - terrainFeatureProcessor->UpdateTerrainData( - transform, m_currentSettings.m_worldBounds, m_currentSettings.m_heightQueryResolution.GetX(), width, height, pixels); + 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); + } + } } } diff --git a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h index c52165da6b..a9240e02a6 100644 --- a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h +++ b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h @@ -94,8 +94,11 @@ namespace Terrain float x, float y, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const override; private: + void ClampPosition(float x, float y, AZ::Vector2& outPosition, AZ::Vector2& normalizedDelta) const; + float GetHeightSynchronous(float x, float y, Sampler sampler, bool* terrainExistsPtr) const; - AZ::Vector3 GetNormalSynchronous(float x, float y, Sampler sampler, bool* terrainExistsPtr) const; + float GetTerrainAreaHeight(float x, float y, bool& terrainExists) const; + AZ::Vector3 GetNormalSynchronous(float x, float y, Sampler sampler, bool* terrainExistsPtr) const; // AZ::TickBus::Handler overrides ... void OnTick(float deltaTime, AZ::ScriptTimePoint time) override; diff --git a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystemBus.h b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystemBus.h index 1ba63a8f84..cda6d65a1e 100644 --- a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystemBus.h +++ b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystemBus.h @@ -65,27 +65,8 @@ namespace Terrain virtual ~TerrainAreaHeightRequests() = default; - enum SurfacePointDataMask - { - POSITION = 0x01, - NORMAL = 0x02, - SURFACE_WEIGHTS = 0x04, - - DEFAULT = POSITION | NORMAL | SURFACE_WEIGHTS - }; - // Synchronous single input location. The Vector3 input position versions are defined to ignore the input Z value. - - virtual void GetHeight( - const AZ::Vector3& inPosition, - AZ::Vector3& outPosition, - AzFramework::Terrain::TerrainDataRequests::Sampler sampleFilter = - AzFramework::Terrain::TerrainDataRequests::Sampler::DEFAULT) = 0; - virtual void GetNormal( - const AZ::Vector3& inPosition, - AZ::Vector3& outNormal, - AzFramework::Terrain::TerrainDataRequests::Sampler sampleFilter = - AzFramework::Terrain::TerrainDataRequests::Sampler::DEFAULT) = 0; + virtual void GetHeight(const AZ::Vector3& inPosition, AZ::Vector3& outPosition, bool& terrainExists) = 0; }; using TerrainAreaHeightRequestBus = AZ::EBus; diff --git a/Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp b/Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp index f0533322c8..ee4b18e2fb 100644 --- a/Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp +++ b/Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp @@ -15,7 +15,12 @@ #include #include -#include +#include +#include + +using ::testing::NiceMock; +using ::testing::AtLeast; +using ::testing::_; using ::testing::NiceMock; using ::testing::AtLeast; @@ -29,7 +34,7 @@ protected: AZStd::unique_ptr m_entity; Terrain::TerrainLayerSpawnerComponent* m_layerSpawnerComponent; - UnitTest::MockBoxShapeComponent* m_shapeComponent; + UnitTest::MockAxisAlignedBoxShapeComponent* m_shapeComponent; AZStd::unique_ptr> m_terrainSystem; void SetUp() override @@ -67,7 +72,7 @@ protected: m_layerSpawnerComponent = m_entity->CreateComponent(config); m_app.RegisterComponentDescriptor(m_layerSpawnerComponent->CreateDescriptor()); - m_shapeComponent = m_entity->CreateComponent(); + m_shapeComponent = m_entity->CreateComponent(); m_app.RegisterComponentDescriptor(m_shapeComponent->CreateDescriptor()); ASSERT_TRUE(m_layerSpawnerComponent); diff --git a/Gems/Terrain/Code/Tests/MockAxisAlignedBoxShapeComponent.h b/Gems/Terrain/Code/Tests/MockAxisAlignedBoxShapeComponent.h new file mode 100644 index 0000000000..aeaaa609c4 --- /dev/null +++ b/Gems/Terrain/Code/Tests/MockAxisAlignedBoxShapeComponent.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ +#pragma once + +#include + +#include +#include +#include +#include + +namespace UnitTest +{ + class MockAxisAlignedBoxShapeComponent + : public AZ::Component + { + public: + AZ_COMPONENT(MockAxisAlignedBoxShapeComponent, "{77CBEED3-FAA3-4BC7-85A9-1A2BFC37BC2A}"); + + static void Reflect([[maybe_unused]] AZ::ReflectContext* context) + { + } + + void Activate() override + { + } + + void Deactivate() override + { + } + + private: + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.push_back(AZ_CRC_CE("ShapeService")); + provided.push_back(AZ_CRC_CE("BoxShapeService")); + provided.push_back(AZ_CRC_CE("AxisAlignedBoxShapeService")); + } + }; +} diff --git a/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp b/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp index 3e557c66a6..91a9744f0b 100644 --- a/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp +++ b/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp @@ -9,20 +9,23 @@ #include #include -#include - #include -#include + +#include +#include +#include + +#include +#include using ::testing::AtLeast; using ::testing::NiceMock; +using ::testing::Return; class TerrainSystemTest : public ::testing::Test { protected: AZ::ComponentApplication m_app; - - AZStd::unique_ptr m_entity; AZStd::unique_ptr m_terrainSystem; void SetUp() override @@ -41,28 +44,46 @@ protected: m_app.Destroy(); } - void CreateEntity() + AZStd::unique_ptr CreateEntity() { - m_entity = AZStd::make_unique(); - m_entity->Init(); - - ASSERT_TRUE(m_entity); + return AZStd::make_unique(); } - void ResetEntity() + void ActivateEntity(AZ::Entity* entity) { - m_entity->Deactivate(); - m_entity->Reset(); + entity->Init(); + EXPECT_EQ(AZ::Entity::State::Init, entity->GetState()); + + entity->Activate(); + EXPECT_EQ(AZ::Entity::State::Active, entity->GetState()); + } + + template + AZ::Component* CreateComponent(AZ::Entity* entity, const Configuration& config) + { + m_app.RegisterComponentDescriptor(Component::CreateDescriptor()); + return entity->CreateComponent(config); + } + + template + AZ::Component* CreateComponent(AZ::Entity* entity) + { + m_app.RegisterComponentDescriptor(Component::CreateDescriptor()); + return entity->CreateComponent(); } }; TEST_F(TerrainSystemTest, TrivialCreateDestroy) { + // Trivially verify that the terrain system can successfully be constructed and destructed without errors. + m_terrainSystem = AZStd::make_unique(); } TEST_F(TerrainSystemTest, TrivialActivateDeactivate) { + // Verify that the terrain system can be activated and deactivated without errors. + m_terrainSystem = AZStd::make_unique(); m_terrainSystem->Activate(); m_terrainSystem->Deactivate(); @@ -70,6 +91,8 @@ TEST_F(TerrainSystemTest, TrivialActivateDeactivate) TEST_F(TerrainSystemTest, CreateEventsCalledOnActivation) { + // Verify that when the terrain system is activated, the OnTerrainDataCreate* ebus notifications are generated. + NiceMock mockTerrainListener; EXPECT_CALL(mockTerrainListener, OnTerrainDataCreateBegin()).Times(AtLeast(1)); EXPECT_CALL(mockTerrainListener, OnTerrainDataCreateEnd()).Times(AtLeast(1)); @@ -80,6 +103,8 @@ TEST_F(TerrainSystemTest, CreateEventsCalledOnActivation) TEST_F(TerrainSystemTest, DestroyEventsCalledOnDeactivation) { + // Verify that when the terrain system is deactivated, the OnTerrainDataDestroy* ebus notifications are generated. + NiceMock mockTerrainListener; EXPECT_CALL(mockTerrainListener, OnTerrainDataDestroyBegin()).Times(AtLeast(1)); EXPECT_CALL(mockTerrainListener, OnTerrainDataDestroyEnd()).Times(AtLeast(1)); @@ -89,4 +114,115 @@ TEST_F(TerrainSystemTest, DestroyEventsCalledOnDeactivation) m_terrainSystem->Deactivate(); } +TEST_F(TerrainSystemTest, TerrainDoesNotExistWhenNoTerrainLayerSpawnersAreRegistered) +{ + // For the terrain system, terrain should only exist where terrain layer spawners are present. + + // Verify that in the active terrain system, if there are no terrain layer spawners, any arbitrary point + // 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{}); + + AZ::Aabb worldBounds = m_terrainSystem->GetTerrainAabb(); + + // Loop through several points within the world bounds, including on the edges, and verify that they all return false for + // terrainExists with default heights and normals. + for (float y = worldBounds.GetMin().GetY(); y <= worldBounds.GetMax().GetY(); y += (worldBounds.GetExtents().GetY() / 4.0f)) + { + for (float x = worldBounds.GetMin().GetX(); x <= worldBounds.GetMax().GetX(); x += (worldBounds.GetExtents().GetX() / 4.0f)) + { + AZ::Vector3 position(x, y, 0.0f); + bool terrainExists = true; + float height = m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists); + EXPECT_FALSE(terrainExists); + EXPECT_EQ(height, worldBounds.GetMin().GetZ()); + + terrainExists = true; + AZ::Vector3 normal = m_terrainSystem->GetNormal( + position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists); + EXPECT_FALSE(terrainExists); + EXPECT_EQ(normal, AZ::Vector3::CreateAxisZ()); + + bool isHole = m_terrainSystem->GetIsHoleFromFloats( + position.GetX(), position.GetY(), AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT); + EXPECT_TRUE(isHole); + } + } +} + +TEST_F(TerrainSystemTest, TerrainExistsOnlyWithinTerrainLayerSpawnerBounds) +{ + // Verify that the presence of a TerrainLayerSpawner causes terrain to exist in (and *only* in) the box where the TerrainLayerSpawner + // is defined. + + // 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()); + + // 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 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. + const AZ::Aabb encompassingBox = + AZ::Aabb::CreateFromMinMax(spawnerBox.GetMin() - (spawnerBox.GetExtents() / 2.0f), + spawnerBox.GetMax() + (spawnerBox.GetExtents() / 2.0f)); + + for (float y = encompassingBox.GetMin().GetY(); y < encompassingBox.GetMax().GetY(); y += 1.0f) + { + for (float x = encompassingBox.GetMin().GetX(); x < encompassingBox.GetMax().GetX(); x += 1.0f) + { + AZ::Vector3 position(x, y, 0.0f); + bool heightQueryTerrainExists = false; + float height = + m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &heightQueryTerrainExists); + bool isHole = m_terrainSystem->GetIsHoleFromFloats( + position.GetX(), position.GetY(), AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT); + + if (spawnerBox.Contains(AZ::Vector3(position.GetX(), position.GetY(), spawnerBox.GetMin().GetZ()))) + { + EXPECT_TRUE(heightQueryTerrainExists); + EXPECT_FALSE(isHole); + EXPECT_EQ(height, spawnerHeight); + } + else + { + EXPECT_FALSE(heightQueryTerrainExists); + EXPECT_TRUE(isHole); + } + } + } +} diff --git a/Gems/Terrain/Code/terrain_mocks_files.cmake b/Gems/Terrain/Code/terrain_mocks_files.cmake new file mode 100644 index 0000000000..2aedd1c5d8 --- /dev/null +++ b/Gems/Terrain/Code/terrain_mocks_files.cmake @@ -0,0 +1,11 @@ +# +# 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 +# +# + +set(FILES + Mocks/Terrain/MockTerrain.h +) diff --git a/Gems/Terrain/Code/terrain_tests_files.cmake b/Gems/Terrain/Code/terrain_tests_files.cmake index 6d1cf97fd9..3ce1d05003 100644 --- a/Gems/Terrain/Code/terrain_tests_files.cmake +++ b/Gems/Terrain/Code/terrain_tests_files.cmake @@ -7,8 +7,8 @@ # set(FILES - Tests/TerrainMocks.h Tests/TerrainTest.cpp Tests/TerrainSystemTest.cpp Tests/LayerSpawnerTests.cpp + Tests/MockAxisAlignedBoxShapeComponent.h )