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:
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user