4b2dc9b3a4
* Remove the "TEST_SUPPORTED" traits. Terrain unit tests should be usable on all platforms, so they shouldn't need a platform-specific trait to enable/disable. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Fix a few misc terrain bugs. * Change Activate/Deactivate to happen immediately instead of deferring. There were too many order-of-operation bugs caused by trying to defer this. * Added implementation for calculating normals. * Fixed bug where GetHeightSynchronous wasn't stopping at the highest-priority layer. * Added locks for SurfaceData bus to help ensure we lock our mutexes in the correct order and avoid deadlocks. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Add trivial TerrainSystem tests. Tests construction, Activate(), Deactivate(), and destruction. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Unified Terrain system calls on single bus. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Added mock for TerrainDataNotificationBus listener. Also added unit tests to verify the listener, and added in missing notification events. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Removed extra Sampler class. Fixed up APIs to correctly pass Sampler and terrainExistsPtr around. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Switched MockTerrainSystem to be proper gmock. This makes it for flexible to use and easier to reuse from other test environments. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Fix settings bug caused by bad order of operations that occurred when the methods moved to a different bus. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Eliminate extra EBus by simplifying area initialization. Previously, there was a back-and-forth ebus signal used for the terrain system to find any terrain spawners that were created prior to the terrain system activation. Now it uses the more simple technique of just grabbing all the spawners that are currently hooked up to the spawner ebus. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Switch to NiceMock so that "uninteresting" mock calls get ignored. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Addressed PR feedback. Filled in terrainExistsPtr at the end, and added it to GetNormal as well. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Fixed shader height calculation. It was off by half a pixel, and it was interpolating, both of which were wrong. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Create initial LmbrCentral mocks that other Gems can use. To help improve mock maintenance over time, this creates mocks in the same Gem as the systems being mocked, instead of the other Gems that need to use mocked systems during testing. This way, the mocks should more easily stay in sync with the interface that they mock out. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Null-guard any uses of Atom to make the class easier to unit test. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Add more mocked terrain services Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Moved Terrain mocks to publicly-available Mocks directory. Also added more unit tests. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Change debug code to use EXACT instead of BILINEAR height sampling, because it's specifically using the terrain grid. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Added support for the sampler filters. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Fix bad merge. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Add unit test to verify terrain layers define terrain regions. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Moved the AABB component mock into a private terrain header, since it's a specialized mock just for the terrain tests. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Moved sampler and normal calculations into TerrainSystem so that they can work across multiple adjacent areas. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Missed a couple of unit test changes. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Removed normal query as well, that needs a seprate unit test. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Removed unused variable that was only caught in Linux/Android builds. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Addressed PR feedback Made the parameter names consistently listed in MockShapes.h. Added comments to TerrainHeightGradientListComponent explaining why terrainExists is always true when a gradient exists. Also fixed a bug where terrainExists should technically be *false* if no gradient exists. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Added more descriptive comments and names for ClampPosition. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Unit tests to verify height samplers work correctly. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Fix bug where wireframe didn't refresh when the setting changed. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Created TerrainWorldRenderer Component and moved all terrain rendering management code into it. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Fixed the icon references Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Removed expensive profile marker. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Addressed PR feedback Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Fixed AR error - constexpr doesn't need to be in a lambda capture. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com>
476 lines
23 KiB
C++
476 lines
23 KiB
C++
/*
|
|
* 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 <AzCore/Component/ComponentApplication.h>
|
|
#include <AzCore/Memory/MemoryComponent.h>
|
|
|
|
#include <AzTest/AzTest.h>
|
|
|
|
#include <TerrainSystem/TerrainSystem.h>
|
|
#include <Components/TerrainLayerSpawnerComponent.h>
|
|
#include <Components/TerrainHeightGradientListComponent.h>
|
|
|
|
#include <Terrain/MockTerrain.h>
|
|
#include <MockAxisAlignedBoxShapeComponent.h>
|
|
|
|
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<Terrain::TerrainSystem> m_terrainSystem;
|
|
|
|
AZStd::unique_ptr<NiceMock<UnitTest::MockBoxShapeComponentRequests>> m_boxShapeRequests;
|
|
AZStd::unique_ptr<NiceMock<UnitTest::MockShapeComponentRequests>> m_shapeRequests;
|
|
AZStd::unique_ptr<NiceMock<UnitTest::MockTerrainAreaHeightRequests>> m_terrainAreaHeightRequests;
|
|
|
|
|
|
void SetUp() override
|
|
{
|
|
AZ::ComponentApplication::Descriptor appDesc;
|
|
appDesc.m_memoryBlocksByteSize = 20 * 1024 * 1024;
|
|
appDesc.m_recordingMode = AZ::Debug::AllocationRecords::RECORD_NO_RECORDS;
|
|
appDesc.m_stackRecordLevels = 20;
|
|
|
|
m_app.Create(appDesc);
|
|
}
|
|
|
|
void TearDown() override
|
|
{
|
|
m_terrainSystem.reset();
|
|
m_boxShapeRequests.reset();
|
|
m_shapeRequests.reset();
|
|
m_terrainAreaHeightRequests.reset();
|
|
m_app.Destroy();
|
|
}
|
|
|
|
AZStd::unique_ptr<AZ::Entity> CreateEntity()
|
|
{
|
|
return AZStd::make_unique<AZ::Entity>();
|
|
}
|
|
|
|
void ActivateEntity(AZ::Entity* entity)
|
|
{
|
|
entity->Init();
|
|
EXPECT_EQ(AZ::Entity::State::Init, entity->GetState());
|
|
|
|
entity->Activate();
|
|
EXPECT_EQ(AZ::Entity::State::Active, entity->GetState());
|
|
}
|
|
|
|
template<typename Component, typename Configuration>
|
|
AZ::Component* CreateComponent(AZ::Entity* entity, const Configuration& config)
|
|
{
|
|
m_app.RegisterComponentDescriptor(Component::CreateDescriptor());
|
|
return entity->CreateComponent<Component>(config);
|
|
}
|
|
|
|
template<typename Component>
|
|
AZ::Component* CreateComponent(AZ::Entity* entity)
|
|
{
|
|
m_app.RegisterComponentDescriptor(Component::CreateDescriptor());
|
|
return entity->CreateComponent<Component>();
|
|
}
|
|
|
|
// 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<Terrain::TerrainSystem>();
|
|
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<AZ::Entity> CreateAndActivateMockTerrainLayerSpawner(
|
|
const AZ::Aabb& spawnerBox,
|
|
const AZStd::function<void(AZ::Vector3& position, bool& terrainExists)>& mockHeights)
|
|
{
|
|
// Create the base entity with a mock box shape, Terrain Layer Spawner, and height provider.
|
|
auto entity = CreateEntity();
|
|
CreateComponent<UnitTest::MockAxisAlignedBoxShapeComponent>(entity.get());
|
|
CreateComponent<Terrain::TerrainLayerSpawnerComponent>(entity.get());
|
|
|
|
m_boxShapeRequests = AZStd::make_unique<NiceMock<UnitTest::MockBoxShapeComponentRequests>>(entity->GetId());
|
|
m_shapeRequests = AZStd::make_unique<NiceMock<UnitTest::MockShapeComponentRequests>>(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<NiceMock<UnitTest::MockTerrainAreaHeightRequests>>(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)
|
|
{
|
|
// Trivially verify that the terrain system can successfully be constructed and destructed without errors.
|
|
|
|
m_terrainSystem = AZStd::make_unique<Terrain::TerrainSystem>();
|
|
}
|
|
|
|
TEST_F(TerrainSystemTest, TrivialActivateDeactivate)
|
|
{
|
|
// Verify that the terrain system can be activated and deactivated without errors.
|
|
|
|
m_terrainSystem = AZStd::make_unique<Terrain::TerrainSystem>();
|
|
m_terrainSystem->Activate();
|
|
m_terrainSystem->Deactivate();
|
|
}
|
|
|
|
TEST_F(TerrainSystemTest, CreateEventsCalledOnActivation)
|
|
{
|
|
// Verify that when the terrain system is activated, the OnTerrainDataCreate* ebus notifications are generated.
|
|
|
|
NiceMock<UnitTest::MockTerrainDataNotificationListener> mockTerrainListener;
|
|
EXPECT_CALL(mockTerrainListener, OnTerrainDataCreateBegin()).Times(AtLeast(1));
|
|
EXPECT_CALL(mockTerrainListener, OnTerrainDataCreateEnd()).Times(AtLeast(1));
|
|
|
|
m_terrainSystem = AZStd::make_unique<Terrain::TerrainSystem>();
|
|
m_terrainSystem->Activate();
|
|
}
|
|
|
|
TEST_F(TerrainSystemTest, DestroyEventsCalledOnDeactivation)
|
|
{
|
|
// Verify that when the terrain system is deactivated, the OnTerrainDataDestroy* ebus notifications are generated.
|
|
|
|
NiceMock<UnitTest::MockTerrainDataNotificationListener> mockTerrainListener;
|
|
EXPECT_CALL(mockTerrainListener, OnTerrainDataDestroyBegin()).Times(AtLeast(1));
|
|
EXPECT_CALL(mockTerrainListener, OnTerrainDataDestroyEnd()).Times(AtLeast(1));
|
|
|
|
m_terrainSystem = AZStd::make_unique<Terrain::TerrainSystem>();
|
|
m_terrainSystem->Activate();
|
|
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 and activate the terrain system with our testing defaults for world bounds and query resolution.
|
|
CreateAndActivateTerrainSystem();
|
|
|
|
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_FLOAT_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 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 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.
|
|
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_FLOAT_EQ(height, spawnerHeight);
|
|
}
|
|
else
|
|
{
|
|
EXPECT_FALSE(heightQueryTerrainExists);
|
|
EXPECT_TRUE(isHole);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|