Terrain System bugfixes (#4180)
* 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>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 <gmock/gmock.h>
|
||||
|
||||
#include <AzCore/Component/ComponentApplication.h>
|
||||
#include <LmbrCentral/Shape/BoxShapeComponentBus.h>
|
||||
#include <LmbrCentral/Shape/ShapeComponentBus.h>
|
||||
|
||||
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));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,4 +8,7 @@
|
||||
|
||||
#include <AzTest/AzTest.h>
|
||||
|
||||
// Include any public mocks here to ensure they get compiled as a part of the test project.
|
||||
#include <LmbrCentral/Shape/MockShapes.h>
|
||||
|
||||
AZ_UNIT_TEST_HOOK(DEFAULT_UNIT_TEST_ENV);
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
+36
-50
@@ -11,59 +11,9 @@
|
||||
|
||||
#include <AzCore/Component/ComponentApplication.h>
|
||||
#include <AzFramework/Terrain/TerrainDataRequestBus.h>
|
||||
#include <LmbrCentral/Shape/ShapeComponentBus.h>
|
||||
|
||||
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());
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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<AZStd::shared_mutex> 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<AZStd::shared_mutex> 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<AZStd::shared_mutex> 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<TerrainFeatureProcessor>();
|
||||
|
||||
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)
|
||||
{
|
||||
terrainFeatureProcessor->UpdateTerrainData(
|
||||
transform, m_currentSettings.m_worldBounds, m_currentSettings.m_heightQueryResolution.GetX(), width, height,
|
||||
pixels);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<TerrainAreaHeightRequests>;
|
||||
|
||||
@@ -15,7 +15,12 @@
|
||||
#include <LmbrCentral/Shape/BoxShapeComponentBus.h>
|
||||
#include <AzTest/AzTest.h>
|
||||
|
||||
#include <TerrainMocks.h>
|
||||
#include <Terrain/MockTerrain.h>
|
||||
#include <MockAxisAlignedBoxShapeComponent.h>
|
||||
|
||||
using ::testing::NiceMock;
|
||||
using ::testing::AtLeast;
|
||||
using ::testing::_;
|
||||
|
||||
using ::testing::NiceMock;
|
||||
using ::testing::AtLeast;
|
||||
@@ -29,7 +34,7 @@ protected:
|
||||
|
||||
AZStd::unique_ptr<AZ::Entity> m_entity;
|
||||
Terrain::TerrainLayerSpawnerComponent* m_layerSpawnerComponent;
|
||||
UnitTest::MockBoxShapeComponent* m_shapeComponent;
|
||||
UnitTest::MockAxisAlignedBoxShapeComponent* m_shapeComponent;
|
||||
AZStd::unique_ptr<NiceMock<UnitTest::MockTerrainSystemService>> m_terrainSystem;
|
||||
|
||||
void SetUp() override
|
||||
@@ -67,7 +72,7 @@ protected:
|
||||
m_layerSpawnerComponent = m_entity->CreateComponent<Terrain::TerrainLayerSpawnerComponent>(config);
|
||||
m_app.RegisterComponentDescriptor(m_layerSpawnerComponent->CreateDescriptor());
|
||||
|
||||
m_shapeComponent = m_entity->CreateComponent<UnitTest::MockBoxShapeComponent>();
|
||||
m_shapeComponent = m_entity->CreateComponent<UnitTest::MockAxisAlignedBoxShapeComponent>();
|
||||
m_app.RegisterComponentDescriptor(m_shapeComponent->CreateDescriptor());
|
||||
|
||||
ASSERT_TRUE(m_layerSpawnerComponent);
|
||||
|
||||
@@ -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 <gmock/gmock.h>
|
||||
|
||||
#include <AzCore/Component/ComponentApplication.h>
|
||||
#include <AzFramework/Terrain/TerrainDataRequestBus.h>
|
||||
#include <LmbrCentral/Shape/ShapeComponentBus.h>
|
||||
#include <LmbrCentral/Shape/MockShapes.h>
|
||||
|
||||
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"));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -9,20 +9,23 @@
|
||||
#include <AzCore/Component/ComponentApplication.h>
|
||||
#include <AzCore/Memory/MemoryComponent.h>
|
||||
|
||||
#include <TerrainSystem/TerrainSystem.h>
|
||||
|
||||
#include <AzTest/AzTest.h>
|
||||
#include <TerrainMocks.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::NiceMock;
|
||||
using ::testing::Return;
|
||||
|
||||
class TerrainSystemTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
AZ::ComponentApplication m_app;
|
||||
|
||||
AZStd::unique_ptr<AZ::Entity> m_entity;
|
||||
AZStd::unique_ptr<Terrain::TerrainSystem> m_terrainSystem;
|
||||
|
||||
void SetUp() override
|
||||
@@ -41,28 +44,46 @@ protected:
|
||||
m_app.Destroy();
|
||||
}
|
||||
|
||||
void CreateEntity()
|
||||
AZStd::unique_ptr<AZ::Entity> CreateEntity()
|
||||
{
|
||||
m_entity = AZStd::make_unique<AZ::Entity>();
|
||||
m_entity->Init();
|
||||
|
||||
ASSERT_TRUE(m_entity);
|
||||
return AZStd::make_unique<AZ::Entity>();
|
||||
}
|
||||
|
||||
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<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>();
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
@@ -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<UnitTest::MockTerrainDataNotificationListener> 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<UnitTest::MockTerrainDataNotificationListener> 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<Terrain::TerrainSystem>();
|
||||
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<UnitTest::MockAxisAlignedBoxShapeComponent>(entity.get());
|
||||
CreateComponent<Terrain::TerrainLayerSpawnerComponent>(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<UnitTest::MockBoxShapeComponentRequests> boxShapeRequests(entity->GetId());
|
||||
NiceMock<UnitTest::MockShapeComponentRequests> 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<UnitTest::MockTerrainAreaHeightRequests> 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<Terrain::TerrainSystem>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -7,8 +7,8 @@
|
||||
#
|
||||
|
||||
set(FILES
|
||||
Tests/TerrainMocks.h
|
||||
Tests/TerrainTest.cpp
|
||||
Tests/TerrainSystemTest.cpp
|
||||
Tests/LayerSpawnerTests.cpp
|
||||
Tests/MockAxisAlignedBoxShapeComponent.h
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user