diff --git a/Gems/Terrain/Code/Include/Terrain/Ebuses/TerrainAreaSurfaceRequestBus.h b/Gems/Terrain/Code/Include/Terrain/Ebuses/TerrainAreaSurfaceRequestBus.h index ed352b55df..9fd5c6b08f 100644 --- a/Gems/Terrain/Code/Include/Terrain/Ebuses/TerrainAreaSurfaceRequestBus.h +++ b/Gems/Terrain/Code/Include/Terrain/Ebuses/TerrainAreaSurfaceRequestBus.h @@ -9,7 +9,7 @@ #pragma once #include - +#include #include namespace Terrain @@ -29,6 +29,11 @@ namespace Terrain //! Get the surfaces and weights from a gradient at a given position. virtual void GetSurfaceWeights(const AZ::Vector3& inPosition, AzFramework::SurfaceData::SurfaceTagWeightList& outSurfaceWeights) const = 0; + + //! Get the surfaces and weights from a gradient at a given list of positions. + virtual void GetSurfaceWeightsFromList( + AZStd::span inPositionList, + AZStd::span outSurfaceWeightsList) const = 0; }; using TerrainAreaSurfaceRequestBus = AZ::EBus; diff --git a/Gems/Terrain/Code/Mocks/Terrain/MockTerrain.h b/Gems/Terrain/Code/Mocks/Terrain/MockTerrain.h index 4704361229..65f2f334bc 100644 --- a/Gems/Terrain/Code/Mocks/Terrain/MockTerrain.h +++ b/Gems/Terrain/Code/Mocks/Terrain/MockTerrain.h @@ -52,6 +52,7 @@ namespace UnitTest } MOCK_METHOD3(GetHeight, void(const AZ::Vector3& inPosition, AZ::Vector3& outPosition, bool& terrainExists)); + MOCK_METHOD2(GetHeights, void(AZStd::span inOutPositionList, AZStd::span terrainExistsList)); }; class MockTerrainSpawnerRequests : public Terrain::TerrainSpawnerRequestBus::Handler diff --git a/Gems/Terrain/Code/Mocks/Terrain/MockTerrainAreaSurfaceRequestBus.h b/Gems/Terrain/Code/Mocks/Terrain/MockTerrainAreaSurfaceRequestBus.h index 665bf581bc..fe201f5c60 100644 --- a/Gems/Terrain/Code/Mocks/Terrain/MockTerrainAreaSurfaceRequestBus.h +++ b/Gems/Terrain/Code/Mocks/Terrain/MockTerrainAreaSurfaceRequestBus.h @@ -29,6 +29,8 @@ namespace UnitTest MOCK_METHOD0(Activate, void()); MOCK_METHOD0(Deactivate, void()); MOCK_CONST_METHOD2(GetSurfaceWeights, void(const AZ::Vector3&, AzFramework::SurfaceData::SurfaceTagWeightList&)); + MOCK_CONST_METHOD2(GetSurfaceWeightsFromList, + void(AZStd::span, AZStd::span)); }; } // namespace UnitTest diff --git a/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.cpp b/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.cpp index b9055375d4..1e7b66ab9d 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.cpp +++ b/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.cpp @@ -164,7 +164,7 @@ namespace Terrain if (!m_isRequestInProgress) { m_isRequestInProgress = true; - GradientSignal::GradientSampleParams params(AZ::Vector3(inPosition.GetX(), inPosition.GetY(), 0.0f)); + GradientSignal::GradientSampleParams params(inPosition); // 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 @@ -189,9 +189,69 @@ namespace Terrain } const float height = AZ::Lerp(m_cachedShapeBounds.GetMin().GetZ(), m_cachedShapeBounds.GetMax().GetZ(), maxSample); - outPosition.SetZ(AZ::GetClamp(height, m_cachedMinWorldHeight, m_cachedMaxWorldHeight)); + outPosition.Set(inPosition.GetX(), inPosition.GetY(), AZ::GetClamp(height, m_cachedMinWorldHeight, m_cachedMaxWorldHeight)); } + void TerrainHeightGradientListComponent::GetHeights( + AZStd::span inOutPositionList, AZStd::span terrainExistsList) + { + AZ_Assert( + inOutPositionList.size() == terrainExistsList.size(), "The position list size doesn't match the terrainExists list size."); + + AZ_WarningOnce("Terrain", !m_isRequestInProgress, "Detected cyclic dependences with terrain height entity references"); + + if (!m_isRequestInProgress) + { + m_isRequestInProgress = true; + + // Start by initializing all our terrainExists flags to false. + AZStd::fill(terrainExistsList.begin(), terrainExistsList.end(), false); + + // Create a temporary buffer for storing all the gradient values for the currently-queried gradient. + AZStd::vector curGradientSamples(inOutPositionList.size()); + + // Create a temporary buffer for storing all the max gradient values. + AZStd::vector maxValueSamples(inOutPositionList.size()); + + // 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 of 0 outside their data bounds if they're using bounded data. We should examine the possibility of extending the + // gradient API to provide actual bounds so that it's possible to detect if the gradient even 'exists' in an area, at which + // point we could just make this list a prioritized list from top to bottom for any points that overlap. + for (auto& gradientId : m_configuration.m_gradientEntities) + { + if (gradientId.IsValid()) + { + GradientSignal::GradientRequestBus::Event( + gradientId, &GradientSignal::GradientRequestBus::Events::GetValues, inOutPositionList, curGradientSamples); + + for (size_t index = 0; index < maxValueSamples.size(); index++) + { + maxValueSamples[index] = AZ::GetMax(maxValueSamples[index], curGradientSamples[index]); + + // 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. + terrainExistsList[index] = true; + } + } + } + + for (size_t index = 0; index < inOutPositionList.size(); index++) + { + if (terrainExistsList[index]) + { + const float height = + AZ::Lerp(m_cachedShapeBounds.GetMin().GetZ(), m_cachedShapeBounds.GetMax().GetZ(), maxValueSamples[index]); + inOutPositionList[index].SetZ(AZ::GetClamp(height, m_cachedMinWorldHeight, m_cachedMaxWorldHeight)); + } + } + + m_isRequestInProgress = false; + } + } + + + void TerrainHeightGradientListComponent::OnCompositionChanged() { RefreshMinMaxHeights(); diff --git a/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.h b/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.h index 12a51b2ac1..c90f4e04d9 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.h +++ b/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.h @@ -68,6 +68,7 @@ namespace Terrain ////////////////////////////////////////////////////////////////////////// // TerrainAreaHeightRequestBus void GetHeight(const AZ::Vector3& inPosition, AZ::Vector3& outPosition, bool& terrainExists) override; + void GetHeights(AZStd::span inOutPositionList, AZStd::span terrainExistsList) override; ////////////////////////////////////////////////////////////////////////// // AZ::Component interface implementation diff --git a/Gems/Terrain/Code/Source/Components/TerrainSurfaceGradientListComponent.cpp b/Gems/Terrain/Code/Source/Components/TerrainSurfaceGradientListComponent.cpp index 4d93842654..7d41b829c1 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainSurfaceGradientListComponent.cpp +++ b/Gems/Terrain/Code/Source/Components/TerrainSurfaceGradientListComponent.cpp @@ -182,7 +182,7 @@ namespace Terrain { outSurfaceWeights.clear(); - const GradientSignal::GradientSampleParams params(AZ::Vector3(inPosition.GetX(), inPosition.GetY(), 0.0f)); + const GradientSignal::GradientSampleParams params(inPosition); for (const auto& mapping : m_configuration.m_gradientSurfaceMappings) { @@ -194,6 +194,27 @@ namespace Terrain } } + void TerrainSurfaceGradientListComponent::GetSurfaceWeightsFromList( + AZStd::span inPositionList, + AZStd::span outSurfaceWeightsList) const + { + AZ_Assert( + inPositionList.size() == outSurfaceWeightsList.size(), "The position list size doesn't match the outSurfaceWeights list size."); + + AZStd::vector gradientValues(inPositionList.size()); + + for (const auto& mapping : m_configuration.m_gradientSurfaceMappings) + { + GradientSignal::GradientRequestBus::Event( + mapping.m_gradientEntityId, &GradientSignal::GradientRequestBus::Events::GetValues, inPositionList, gradientValues); + + for (size_t index = 0; index < outSurfaceWeightsList.size(); index++) + { + outSurfaceWeightsList[index].emplace_back(mapping.m_surfaceTag, gradientValues[index]); + } + } + } + void TerrainSurfaceGradientListComponent::OnCompositionChanged() { TerrainSystemServiceRequestBus::Broadcast( diff --git a/Gems/Terrain/Code/Source/Components/TerrainSurfaceGradientListComponent.h b/Gems/Terrain/Code/Source/Components/TerrainSurfaceGradientListComponent.h index 8a3c097b6c..36c16bed8f 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainSurfaceGradientListComponent.h +++ b/Gems/Terrain/Code/Source/Components/TerrainSurfaceGradientListComponent.h @@ -80,6 +80,9 @@ namespace Terrain // TerrainAreaSurfaceRequestBus void GetSurfaceWeights(const AZ::Vector3& inPosition, AzFramework::SurfaceData::SurfaceTagWeightList& outSurfaceWeights) const override; + void GetSurfaceWeightsFromList( + AZStd::span inPositionList, + AZStd::span outSurfaceWeightsList) const override; private: ////////////////////////////////////////////////////////////////////////// diff --git a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystemBus.h b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystemBus.h index 013d82d94d..2319840e9f 100644 --- a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystemBus.h +++ b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystemBus.h @@ -10,6 +10,7 @@ #include #include +#include #include #include @@ -65,8 +66,16 @@ namespace Terrain virtual ~TerrainAreaHeightRequests() = default; - // Synchronous single input location. The Vector3 input position versions are defined to ignore the input Z value. + /// Synchronous single input location. + /// @inPosition is the input position to query. + /// @outPosition will have the same XY as inPosition, but with the Z adjusted to the proper height. + /// @terrainExists is true if the output position is valid terrain. virtual void GetHeight(const AZ::Vector3& inPosition, AZ::Vector3& outPosition, bool& terrainExists) = 0; + + /// Synchronous multiple input locations. + /// @inOutPositionList takes a list of Vector3s as input and returns the Vector3s with Z filled out. + /// @terrainExistsList outputs flags for whether or not each output position is valid terrain. + virtual void GetHeights(AZStd::span inOutPositionList, AZStd::span terrainExistsList) = 0; }; using TerrainAreaHeightRequestBus = AZ::EBus; diff --git a/Gems/Terrain/Code/Tests/TerrainHeightGradientListTests.cpp b/Gems/Terrain/Code/Tests/TerrainHeightGradientListTests.cpp index 57a4e0ee03..77139a5209 100644 --- a/Gems/Terrain/Code/Tests/TerrainHeightGradientListTests.cpp +++ b/Gems/Terrain/Code/Tests/TerrainHeightGradientListTests.cpp @@ -63,7 +63,7 @@ protected: return heightGradientListComponent; } - void AddRequiredComponetsToEntity(AZ::Entity* entity) + void AddRequiredComponentsToEntity(AZ::Entity* entity) { // Create the required box component. UnitTest::MockAxisAlignedBoxShapeComponent* boxComponent = entity->CreateComponent(); @@ -92,7 +92,7 @@ TEST_F(TerrainHeightGradientListComponentTest, ActivateEntityActivateSuccess) AddHeightGradientListToEntity(entity.get()); - AddRequiredComponetsToEntity(entity.get()); + AddRequiredComponentsToEntity(entity.get()); entity->Activate(); EXPECT_EQ(entity->GetState(), AZ::Entity::State::Active); @@ -105,7 +105,7 @@ TEST_F(TerrainHeightGradientListComponentTest, TerrainHeightGradientRefreshesTer AddHeightGradientListToEntity(entity.get()); - AddRequiredComponetsToEntity(entity.get()); + AddRequiredComponentsToEntity(entity.get()); entity->Activate(); @@ -129,7 +129,7 @@ TEST_F(TerrainHeightGradientListComponentTest, TerrainHeightGradientListReturnsH AddHeightGradientListToEntity(entity.get()); - AddRequiredComponetsToEntity(entity.get()); + AddRequiredComponentsToEntity(entity.get()); NiceMock heightfieldRequestBus(entity->GetId()); @@ -166,3 +166,71 @@ TEST_F(TerrainHeightGradientListComponentTest, TerrainHeightGradientListReturnsH EXPECT_NEAR(height, mockGradientValue * max, 0.01f); } +TEST_F(TerrainHeightGradientListComponentTest, TerrainHeightGradientListGetHeightAndGetHeightsMatch) +{ + // Check that the HeightGradientListComponent returns the same height values from GetHeight as GetHeights. + + auto entity = CreateEntity(); + AddHeightGradientListToEntity(entity.get()); + AddRequiredComponentsToEntity(entity.get()); + + NiceMock heightfieldRequestBus(entity->GetId()); + + entity->Activate(); + + // Create a deterministic but varying result for our mock gradient. + NiceMock gradientRequests(entity->GetId()); + ON_CALL(gradientRequests, GetValue) + .WillByDefault( + [](const GradientSignal::GradientSampleParams& params) -> float + { + double intpart; + return aznumeric_cast(modf(params.m_position.GetX(), &intpart)); + }); + + // Setup a mock to provide the encompassing Aabb to the HeightGradientListComponent. + const float min = 0.0f; + const float max = 1000.0f; + const AZ::Aabb aabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3(min), AZ::Vector3(max)); + NiceMock mockShapeRequests(entity->GetId()); + ON_CALL(mockShapeRequests, GetEncompassingAabb).WillByDefault(Return(aabb)); + + const float worldMax = 10000.0f; + const AZ::Aabb worldAabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3(min), AZ::Vector3(worldMax)); + NiceMock mockterrainDataRequests; + ON_CALL(mockterrainDataRequests, GetTerrainHeightQueryResolution).WillByDefault(Return(AZ::Vector2(1.0f))); + ON_CALL(mockterrainDataRequests, GetTerrainAabb).WillByDefault(Return(worldAabb)); + + // Ensure the cached values in the HeightGradientListComponent are up to date. + LmbrCentral::DependencyNotificationBus::Event(entity->GetId(), &LmbrCentral::DependencyNotificationBus::Events::OnCompositionChanged); + + AZStd::vector inOutPositions; + AZStd::vector terrainExistsList; + + // Build up a list of input positions to query with. + for (float y = 0.0f; y <= 10.0f; y += 0.1f) + { + for (float x = 0.0f; x <= 10.0f; x += 0.1f) + { + inOutPositions.emplace_back(x, y, 0.0f); + terrainExistsList.emplace_back(false); + } + } + + // Get the values from GetHeights + Terrain::TerrainAreaHeightRequestBus::Event( + entity->GetId(), &Terrain::TerrainAreaHeightRequestBus::Events::GetHeights, inOutPositions, terrainExistsList); + + // For each result returned from GetHeights, verify that it matches the result from GetHeight + for (size_t index = 0; index < inOutPositions.size(); index++) + { + AZ::Vector3 inPosition(inOutPositions[index].GetX(), inOutPositions[index].GetY(), 0.0f); + AZ::Vector3 outPosition = AZ::Vector3(0.0f); + bool terrainExists = false; + Terrain::TerrainAreaHeightRequestBus::Event( + entity->GetId(), &Terrain::TerrainAreaHeightRequestBus::Events::GetHeight, inPosition, outPosition, terrainExists); + + ASSERT_TRUE(inOutPositions[index].IsClose(outPosition)); + ASSERT_EQ(terrainExists, terrainExistsList[index]); + } +} diff --git a/Gems/Terrain/Code/Tests/TerrainSurfaceGradientListTests.cpp b/Gems/Terrain/Code/Tests/TerrainSurfaceGradientListTests.cpp index 71f5d64fa8..b7df6b8bad 100644 --- a/Gems/Terrain/Code/Tests/TerrainSurfaceGradientListTests.cpp +++ b/Gems/Terrain/Code/Tests/TerrainSurfaceGradientListTests.cpp @@ -113,6 +113,68 @@ namespace UnitTest index++; } } + + TEST_F(TerrainSurfaceGradientListTest, SurfaceGradientGetSurfaceWeightsAndGetSurfaceWeightsFromListMatch) + { + // The GetSurfaceWeights and GetSurfaceWeightsFromList APIs should return the same values for the given inputs. + + auto entity = CreateEntity(); + AddRequiredComponentsToEntity(entity.get()); + + // Create a deterministic but varying result for our mock gradient - return the fractional part of the X position. + auto gradientEntity1 = CreateEntity(); + NiceMock mockGradientRequests1(gradientEntity1->GetId()); + ON_CALL(mockGradientRequests1, GetValue) + .WillByDefault( + [](const GradientSignal::GradientSampleParams& params) -> float + { + double intpart; + return aznumeric_cast(modf(params.m_position.GetX(), &intpart)); + }); + + // Return varying result for this mock too, but this time return the Y position fraction. + auto gradientEntity2 = CreateEntity(); + NiceMock mockGradientRequests2(gradientEntity2->GetId()); + ON_CALL(mockGradientRequests2, GetValue) + .WillByDefault( + [](const GradientSignal::GradientSampleParams& params) -> float + { + double intpart; + return aznumeric_cast(modf(params.m_position.GetY(), &intpart)); + }); + + + // Build up a list of input positions to query with. + AZStd::vector inPositions; + for (float y = 0.0f; y <= 10.0f; y += 0.1f) + { + for (float x = 0.0f; x <= 10.0f; x += 0.1f) + { + inPositions.emplace_back(x, y, 0.0f); + } + } + + // Call GetSurfaceWeightsFromList to get the set of output SurfaceWeightList values + AZStd::vector weightsList(inPositions.size()); + Terrain::TerrainAreaSurfaceRequestBus::Event( + entity->GetId(), &Terrain::TerrainAreaSurfaceRequestBus::Events::GetSurfaceWeightsFromList, inPositions, weightsList); + + // For each result returned from GetSurfaceWeightsFromList, verify that it matches the result from GetSurfaceWeights + for (size_t index = 0; index < inPositions.size(); index++) + { + AzFramework::SurfaceData::SurfaceTagWeightList weightList; + Terrain::TerrainAreaSurfaceRequestBus::Event( + entity->GetId(), &Terrain::TerrainAreaSurfaceRequestBus::Events::GetSurfaceWeights, inPositions[index], weightList); + + // Verify that we're returning the same values in the same order. + ASSERT_EQ(weightsList[index].size(), weightList.size()); + for (size_t weightIndex = 0; weightIndex < weightsList[index].size(); weightIndex++) + { + ASSERT_EQ(weightsList[index][weightIndex].m_surfaceType, weightList[weightIndex].m_surfaceType); + ASSERT_EQ(weightsList[index][weightIndex].m_weight, weightList[weightIndex].m_weight); + } + } + } } // namespace UnitTest