Add GetHeights() and GetSurfaceWeightsFromList() APIs. (#7121)

These perform bulk queries of the underlying gradients which is much faster than performing a lot of individual queries.

Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com>
This commit is contained in:
Mike Balfour
2022-01-25 09:52:00 -06:00
committed by GitHub
parent a2db9806a8
commit 95c0186d1b
10 changed files with 241 additions and 9 deletions
@@ -9,7 +9,7 @@
#pragma once
#include <AzCore/Component/ComponentBus.h>
#include <AzCore/std/containers/span.h>
#include <SurfaceData/SurfaceDataTypes.h>
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<const AZ::Vector3> inPositionList,
AZStd::span<AzFramework::SurfaceData::SurfaceTagWeightList> outSurfaceWeightsList) const = 0;
};
using TerrainAreaSurfaceRequestBus = AZ::EBus<TerrainAreaSurfaceRequests>;
@@ -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<AZ::Vector3> inOutPositionList, AZStd::span<bool> terrainExistsList));
};
class MockTerrainSpawnerRequests : public Terrain::TerrainSpawnerRequestBus::Handler
@@ -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<const AZ::Vector3>, AZStd::span<AzFramework::SurfaceData::SurfaceTagWeightList>));
};
} // namespace UnitTest
@@ -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<AZ::Vector3> inOutPositionList, AZStd::span<bool> 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<float> curGradientSamples(inOutPositionList.size());
// Create a temporary buffer for storing all the max gradient values.
AZStd::vector<float> 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();
@@ -68,6 +68,7 @@ namespace Terrain
//////////////////////////////////////////////////////////////////////////
// TerrainAreaHeightRequestBus
void GetHeight(const AZ::Vector3& inPosition, AZ::Vector3& outPosition, bool& terrainExists) override;
void GetHeights(AZStd::span<AZ::Vector3> inOutPositionList, AZStd::span<bool> terrainExistsList) override;
//////////////////////////////////////////////////////////////////////////
// AZ::Component interface implementation
@@ -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<const AZ::Vector3> inPositionList,
AZStd::span<AzFramework::SurfaceData::SurfaceTagWeightList> outSurfaceWeightsList) const
{
AZ_Assert(
inPositionList.size() == outSurfaceWeightsList.size(), "The position list size doesn't match the outSurfaceWeights list size.");
AZStd::vector<float> 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(
@@ -80,6 +80,9 @@ namespace Terrain
// TerrainAreaSurfaceRequestBus
void GetSurfaceWeights(const AZ::Vector3& inPosition, AzFramework::SurfaceData::SurfaceTagWeightList& outSurfaceWeights) const override;
void GetSurfaceWeightsFromList(
AZStd::span<const AZ::Vector3> inPositionList,
AZStd::span<AzFramework::SurfaceData::SurfaceTagWeightList> outSurfaceWeightsList) const override;
private:
//////////////////////////////////////////////////////////////////////////
@@ -10,6 +10,7 @@
#include <AzCore/Math/Vector2.h>
#include <AzCore/Math/Aabb.h>
#include <AzCore/std/containers/span.h>
#include <AzCore/std/functional.h>
#include <AzCore/std/smart_ptr/shared_ptr.h>
@@ -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<AZ::Vector3> inOutPositionList, AZStd::span<bool> terrainExistsList) = 0;
};
using TerrainAreaHeightRequestBus = AZ::EBus<TerrainAreaHeightRequests>;
@@ -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<UnitTest::MockAxisAlignedBoxShapeComponent>();
@@ -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<UnitTest::MockTerrainAreaHeightRequests> 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<UnitTest::MockTerrainAreaHeightRequests> heightfieldRequestBus(entity->GetId());
entity->Activate();
// Create a deterministic but varying result for our mock gradient.
NiceMock<UnitTest::MockGradientRequests> gradientRequests(entity->GetId());
ON_CALL(gradientRequests, GetValue)
.WillByDefault(
[](const GradientSignal::GradientSampleParams& params) -> float
{
double intpart;
return aznumeric_cast<float>(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<UnitTest::MockShapeComponentRequests> 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<UnitTest::MockTerrainDataRequests> 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<AZ::Vector3> inOutPositions;
AZStd::vector<bool> 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]);
}
}
@@ -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<UnitTest::MockGradientRequests> mockGradientRequests1(gradientEntity1->GetId());
ON_CALL(mockGradientRequests1, GetValue)
.WillByDefault(
[](const GradientSignal::GradientSampleParams& params) -> float
{
double intpart;
return aznumeric_cast<float>(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<UnitTest::MockGradientRequests> mockGradientRequests2(gradientEntity2->GetId());
ON_CALL(mockGradientRequests2, GetValue)
.WillByDefault(
[](const GradientSignal::GradientSampleParams& params) -> float
{
double intpart;
return aznumeric_cast<float>(modf(params.m_position.GetY(), &intpart));
});
// Build up a list of input positions to query with.
AZStd::vector<AZ::Vector3> 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<AzFramework::SurfaceData::SurfaceTagWeightList> 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