Terrain System fixes (#4299)

* 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>
This commit is contained in:
Mike Balfour
2021-09-27 14:58:39 -05:00
committed by GitHub
parent 527508ce66
commit 4b2dc9b3a4
13 changed files with 674 additions and 114 deletions
@@ -85,7 +85,6 @@ namespace AZ
{
const auto jobLambda = [&]() -> void
{
AZ_PROFILE_SCOPE(AzRender, "MeshFP::Simulate() Lambda");
for (auto meshDataIter = iteratorRange.first; meshDataIter != iteratorRange.second; ++meshDataIter)
{
if (!meshDataIter->m_model)
@@ -13,8 +13,6 @@
#include <AzCore/Serialization/EditContext.h>
#include <AzCore/Serialization/EditContextConstants.inl>
#include <Atom/RPI.Public/FeatureProcessorFactory.h>
#include <TerrainRenderer/TerrainFeatureProcessor.h>
#include <TerrainSystem/TerrainSystem.h>
namespace Terrain
@@ -34,8 +32,6 @@ namespace Terrain
->Attribute(AZ::Edit::Attributes::AutoExpand, true)
;
}
Terrain::TerrainFeatureProcessor::Reflect(context);
}
}
@@ -49,9 +45,8 @@ namespace Terrain
incompatible.push_back(AZ_CRC_CE("TerrainService"));
}
void TerrainSystemComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required)
void TerrainSystemComponent::GetRequiredServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& required)
{
required.push_back(AZ_CRC_CE("RPISystem"));
}
void TerrainSystemComponent::GetDependentServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& dependent)
@@ -68,14 +63,11 @@ namespace Terrain
// every time an entity is added or removed to a level. If this ever changes, the Terrain System ownership could move into
// the level component.
m_terrainSystem = new TerrainSystem();
AZ::RPI::FeatureProcessorFactory::Get()->RegisterFeatureProcessor<Terrain::TerrainFeatureProcessor>();
}
void TerrainSystemComponent::Deactivate()
{
delete m_terrainSystem;
m_terrainSystem = nullptr;
AZ::RPI::FeatureProcessorFactory::Get()->UnregisterFeatureProcessor<Terrain::TerrainFeatureProcessor>();
}
}
@@ -95,6 +95,8 @@ namespace Terrain
AzFramework::EntityDebugDisplayEventBus::Handler::BusConnect(GetEntityId());
AzFramework::BoundsRequestBus::Handler::BusConnect(GetEntityId());
AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusConnect();
RefreshCachedWireframeGrid(AZ::Aabb::CreateNull());
}
void TerrainWorldDebuggerComponent::Deactivate()
@@ -0,0 +1,213 @@
/*
* 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 <Components/TerrainWorldRendererComponent.h>
#include <AzCore/Component/Entity.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzFramework/Entity/GameEntityContextBus.h>
#include <SurfaceData/SurfaceDataSystemRequestBus.h>
#include <Atom/RPI.Public/Scene.h>
#include <Atom/RPI.Public/FeatureProcessorFactory.h>
#include <TerrainRenderer/TerrainFeatureProcessor.h>
namespace Terrain
{
void TerrainWorldRendererConfig::Reflect(AZ::ReflectContext* context)
{
Terrain::TerrainFeatureProcessor::Reflect(context);
AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context);
if (serialize)
{
serialize->Class<TerrainWorldRendererConfig, AZ::ComponentConfig>()->Version(1);
AZ::EditContext* edit = serialize->GetEditContext();
if (edit)
{
edit->Class<TerrainWorldRendererConfig>("Terrain World Renderer Component", "Enables terrain rendering")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZStd::vector<AZ::Crc32>({ AZ_CRC_CE("Level") }))
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
->Attribute(AZ::Edit::Attributes::AutoExpand, true);
}
}
}
void TerrainWorldRendererComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services)
{
services.push_back(AZ_CRC_CE("TerrainRendererService"));
}
void TerrainWorldRendererComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& services)
{
services.push_back(AZ_CRC_CE("TerrainRendererService"));
}
void TerrainWorldRendererComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& services)
{
services.push_back(AZ_CRC_CE("TerrainService"));
}
void TerrainWorldRendererComponent::Reflect(AZ::ReflectContext* context)
{
TerrainWorldRendererConfig::Reflect(context);
AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context);
if (serialize)
{
serialize->Class<TerrainWorldRendererComponent, AZ::Component>()->Version(0)->Field(
"Configuration", &TerrainWorldRendererComponent::m_configuration);
}
}
TerrainWorldRendererComponent::TerrainWorldRendererComponent(const TerrainWorldRendererConfig& configuration)
: m_configuration(configuration)
{
}
TerrainWorldRendererComponent::~TerrainWorldRendererComponent()
{
if (m_terrainRendererActive)
{
Deactivate();
}
}
AZ::RPI::Scene* TerrainWorldRendererComponent::GetScene() const
{
// Find the entity context for the entity ID.
AzFramework::EntityContextId entityContextId = AzFramework::EntityContextId::CreateNull();
AzFramework::EntityIdContextQueryBus::EventResult(
entityContextId, GetEntityId(), &AzFramework::EntityIdContextQueryBus::Events::GetOwningContextId);
return AZ::RPI::Scene::GetSceneForEntityContextId(entityContextId);
}
void TerrainWorldRendererComponent::Activate()
{
// On component activation, register the terrain feature processor with Atom and the scene related to this entity context.
AZ::RPI::FeatureProcessorFactory::Get()->RegisterFeatureProcessor<Terrain::TerrainFeatureProcessor>();
if (AZ::RPI::Scene* scene = GetScene(); scene)
{
m_terrainFeatureProcessor = scene->EnableFeatureProcessor<Terrain::TerrainFeatureProcessor>();
}
AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusConnect();
m_terrainRendererActive = true;
}
void TerrainWorldRendererComponent::Deactivate()
{
// On component deactivation, unregister the feature processor and remove it from the default scene.
m_terrainRendererActive = false;
AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusDisconnect();
if (AZ::RPI::Scene* scene = GetScene(); scene)
{
if (scene->GetFeatureProcessor<Terrain::TerrainFeatureProcessor>())
{
scene->DisableFeatureProcessor<Terrain::TerrainFeatureProcessor>();
}
}
m_terrainFeatureProcessor = nullptr;
AZ::RPI::FeatureProcessorFactory::Get()->UnregisterFeatureProcessor<Terrain::TerrainFeatureProcessor>();
}
bool TerrainWorldRendererComponent::ReadInConfig(const AZ::ComponentConfig* baseConfig)
{
if (auto config = azrtti_cast<const TerrainWorldRendererConfig*>(baseConfig))
{
m_configuration = *config;
return true;
}
return false;
}
bool TerrainWorldRendererComponent::WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const
{
if (auto config = azrtti_cast<TerrainWorldRendererConfig*>(outBaseConfig))
{
*config = m_configuration;
return true;
}
return false;
}
void TerrainWorldRendererComponent::OnTerrainDataDestroyBegin()
{
// If the terrain is being destroyed, remove all existing terrain data from the feature processor.
if (m_terrainFeatureProcessor)
{
m_terrainFeatureProcessor->RemoveTerrainData();
}
}
void TerrainWorldRendererComponent::OnTerrainDataChanged([[maybe_unused]] const AZ::Aabb& dirtyRegion, [[maybe_unused]] TerrainDataChangedMask dataChangedMask)
{
// Block other threads from accessing the surface data bus while we are in GetValue (which may call into the SurfaceData bus).
// We lock our surface data mutex *before* checking / setting "isRequestInProgress" so that we prevent race conditions
// that create false detection of cyclic dependencies when multiple requests occur on different threads simultaneously.
// (One case where this was previously able to occur was in rapid updating of the Preview widget on the
// GradientSurfaceDataComponent in the Editor when moving the threshold sliders back and forth rapidly)
auto& surfaceDataContext = SurfaceData::SurfaceDataSystemRequestBus::GetOrCreateContext(false);
typename SurfaceData::SurfaceDataSystemRequestBus::Context::DispatchLockGuard scopeLock(surfaceDataContext.m_contextMutex);
AZ::Vector2 queryResolution = AZ::Vector2(1.0f);
AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
queryResolution, &AzFramework::Terrain::TerrainDataRequests::GetTerrainHeightQueryResolution);
AZ::Aabb worldBounds = AZ::Aabb::CreateNull();
AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
worldBounds, &AzFramework::Terrain::TerrainDataRequests::GetTerrainAabb);
AZ::Transform transform = AZ::Transform::CreateTranslation(worldBounds.GetCenter());
uint32_t width = aznumeric_cast<uint32_t>(
(float)worldBounds.GetXExtent() / queryResolution.GetX());
uint32_t height = aznumeric_cast<uint32_t>(
(float)worldBounds.GetYExtent() / queryResolution.GetY());
AZStd::vector<float> pixels;
pixels.resize_no_construct(width * height);
const uint32_t pixelDataSize = width * height * sizeof(float);
memset(pixels.data(), 0, pixelDataSize);
for (uint32_t y = 0; y < height; y++)
{
for (uint32_t x = 0; x < width; x++)
{
bool terrainExists = true;
float terrainHeight = 0.0f;
AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(
terrainHeight, &AzFramework::Terrain::TerrainDataRequests::GetHeightFromFloats,
(x * queryResolution.GetX()) + worldBounds.GetMin().GetX(),
(y * queryResolution.GetY()) + worldBounds.GetMin().GetY(),
AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT,
&terrainExists);
pixels[(y * width) + x] =
(terrainHeight - worldBounds.GetMin().GetZ()) / worldBounds.GetExtents().GetZ();
}
}
if (m_terrainFeatureProcessor)
{
m_terrainFeatureProcessor->UpdateTerrainData(transform, worldBounds, queryResolution.GetX(), width, height, pixels);
}
}
}
@@ -0,0 +1,75 @@
/*
* 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 <AzCore/Component/Component.h>
#include <AzCore/Math/Vector3.h>
#include <TerrainSystem/TerrainSystem.h>
namespace LmbrCentral
{
template<typename, typename>
class EditorWrappedComponentBase;
}
namespace AZ::RPI
{
class Scene;
}
namespace Terrain
{
class TerrainFeatureProcessor;
class TerrainWorldRendererConfig
: public AZ::ComponentConfig
{
public:
AZ_CLASS_ALLOCATOR(TerrainWorldRendererConfig, AZ::SystemAllocator, 0);
AZ_RTTI(TerrainWorldRendererConfig, "{08C5863C-092D-4A69-8226-4978E4F6E343}", AZ::ComponentConfig);
static void Reflect(AZ::ReflectContext* context);
};
class TerrainWorldRendererComponent
: public AZ::Component
, public AzFramework::Terrain::TerrainDataNotificationBus::Handler
{
public:
template<typename, typename>
friend class LmbrCentral::EditorWrappedComponentBase;
AZ_COMPONENT(TerrainWorldRendererComponent, "{3B0DB71E-5944-437C-8C88-70F8B405BFC7}");
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services);
static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& services);
static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& services);
static void Reflect(AZ::ReflectContext* context);
TerrainWorldRendererComponent(const TerrainWorldRendererConfig& configuration);
TerrainWorldRendererComponent() = default;
~TerrainWorldRendererComponent() override;
//////////////////////////////////////////////////////////////////////////
// AZ::Component interface implementation
void Activate() override;
void Deactivate() override;
bool ReadInConfig(const AZ::ComponentConfig* baseConfig) override;
bool WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const override;
protected:
void OnTerrainDataDestroyBegin() override;
void OnTerrainDataChanged(const AZ::Aabb& dirtyRegion, TerrainDataChangedMask dataChangedMask) override;
AZ::RPI::Scene* GetScene() const;
private:
TerrainWorldRendererConfig m_configuration;
bool m_terrainRendererActive{ false };
TerrainFeatureProcessor* m_terrainFeatureProcessor{ nullptr };
};
}
@@ -0,0 +1,56 @@
/*
* 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 <EditorComponents/EditorTerrainWorldRendererComponent.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Serialization/EditContext.h>
namespace Terrain
{
void EditorTerrainWorldRendererComponent::Reflect(AZ::ReflectContext* context)
{
BaseClassType::Reflect(context);
AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
if (serializeContext)
{
serializeContext->Class<EditorTerrainWorldRendererComponent, BaseClassType>()
->Version(0)
;
if (auto editContext = serializeContext->GetEditContext())
{
editContext->Class<EditorTerrainWorldRendererComponent>(
"Terrain World Renderer", "")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::Category, "Terrain")
->Attribute(AZ::Edit::Attributes::Icon, "Editor/Icons/Components/TerrainWorldRenderer.svg")
->Attribute(AZ::Edit::Attributes::ViewportIcon, "Editor/Icons/Components/Viewport/TerrainWorldRenderer.svg")
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZStd::vector<AZ::Crc32>({ AZ_CRC_CE("Level") }))
;
}
}
}
void EditorTerrainWorldRendererComponent::Init()
{
BaseClassType::Init();
}
void EditorTerrainWorldRendererComponent::Activate()
{
BaseClassType::Activate();
}
AZ::u32 EditorTerrainWorldRendererComponent::ConfigurationChanged()
{
return BaseClassType::ConfigurationChanged();
}
}
@@ -0,0 +1,38 @@
/*
* 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 <Components/TerrainWorldRendererComponent.h>
#include <AzToolsFramework/ToolsComponents/EditorComponentBase.h>
#include <LmbrCentral/Component/EditorWrappedComponentBase.h>
namespace Terrain
{
class EditorTerrainWorldRendererComponent
: public LmbrCentral::EditorWrappedComponentBase<TerrainWorldRendererComponent, TerrainWorldRendererConfig>
{
public:
using BaseClassType = LmbrCentral::EditorWrappedComponentBase<TerrainWorldRendererComponent, TerrainWorldRendererConfig>;
AZ_EDITOR_COMPONENT(EditorTerrainWorldRendererComponent, "{7BEFF763-89A6-4EDA-B199-B049A8E757AF}", BaseClassType);
static void Reflect(AZ::ReflectContext* context);
//////////////////////////////////////////////////////////////////////////
// AZ::Component interface implementation
void Init() override;
void Activate() override;
AZ::u32 ConfigurationChanged() override;
protected:
using BaseClassType::m_configuration;
using BaseClassType::m_component;
using BaseClassType::m_visible;
private:
};
}
@@ -12,6 +12,7 @@
#include <EditorComponents/EditorTerrainSystemComponent.h>
#include <EditorComponents/EditorTerrainWorldComponent.h>
#include <EditorComponents/EditorTerrainWorldDebuggerComponent.h>
#include <EditorComponents/EditorTerrainWorldRendererComponent.h>
namespace Terrain
{
@@ -25,6 +26,7 @@ namespace Terrain
Terrain::EditorTerrainSystemComponent::CreateDescriptor(),
Terrain::EditorTerrainWorldComponent::CreateDescriptor(),
Terrain::EditorTerrainWorldDebuggerComponent::CreateDescriptor(),
Terrain::EditorTerrainWorldRendererComponent::CreateDescriptor(),
});
}
@@ -13,6 +13,7 @@
#include <Components/TerrainSystemComponent.h>
#include <Components/TerrainWorldComponent.h>
#include <Components/TerrainWorldDebuggerComponent.h>
#include <Components/TerrainWorldRendererComponent.h>
#include <Components/TerrainHeightGradientListComponent.h>
#include <Components/TerrainLayerSpawnerComponent.h>
#include <Components/TerrainSurfaceDataSystemComponent.h>
@@ -26,6 +27,7 @@ namespace Terrain
TerrainSystemComponent::CreateDescriptor(),
TerrainWorldComponent::CreateDescriptor(),
TerrainWorldDebuggerComponent::CreateDescriptor(),
TerrainWorldRendererComponent::CreateDescriptor(),
TerrainHeightGradientListComponent::CreateDescriptor(),
TerrainLayerSpawnerComponent::CreateDescriptor(),
TerrainSurfaceDataSystemComponent::CreateDescriptor(),
@@ -12,10 +12,6 @@
#include <SurfaceData/SurfaceDataSystemRequestBus.h>
#include <LmbrCentral/Shape/ShapeComponentBus.h>
#include <Atom/RPI.Public/Scene.h>
#include <Atom/RPI.Public/FeatureProcessorFactory.h>
#include <TerrainRenderer/TerrainFeatureProcessor.h>
using namespace Terrain;
bool TerrainLayerPriorityComparator::operator()(const AZ::EntityId& layer1id, const AZ::EntityId& layer2id) const
@@ -114,18 +110,6 @@ void TerrainSystem::Deactivate()
m_terrainSettingsDirty = true;
m_requestedSettings.m_systemActive = false;
if (auto rpi = AZ::RPI::RPISystemInterface::Get(); rpi)
{
if (auto defaultScene = rpi->GetDefaultScene(); defaultScene)
{
const AZ::RPI::Scene* scene = defaultScene.get();
if (auto terrainFeatureProcessor = scene->GetFeatureProcessor<TerrainFeatureProcessor>(); terrainFeatureProcessor)
{
terrainFeatureProcessor->RemoveTerrainData();
}
}
}
AzFramework::Terrain::TerrainDataNotificationBus::Broadcast(
&AzFramework::Terrain::TerrainDataNotificationBus::Events::OnTerrainDataDestroyEnd);
}
@@ -507,60 +491,6 @@ void TerrainSystem::OnTick(float /*deltaTime*/, AZ::ScriptTimePoint /*time*/)
m_currentSettings = m_requestedSettings;
}
if (m_currentSettings.m_systemActive && m_terrainHeightDirty)
{
AZStd::shared_lock<AZStd::shared_mutex> lock(m_areaMutex);
// Block other threads from accessing the surface data bus while we are in GetValue (which may call into the SurfaceData bus).
// We lock our surface data mutex *before* checking / setting "isRequestInProgress" so that we prevent race conditions
// that create false detection of cyclic dependencies when multiple requests occur on different threads simultaneously.
// (One case where this was previously able to occur was in rapid updating of the Preview widget on the
// GradientSurfaceDataComponent in the Editor when moving the threshold sliders back and forth rapidly)
auto& surfaceDataContext = SurfaceData::SurfaceDataSystemRequestBus::GetOrCreateContext(false);
typename SurfaceData::SurfaceDataSystemRequestBus::Context::DispatchLockGuard scopeLock(surfaceDataContext.m_contextMutex);
AZ::Transform transform = AZ::Transform::CreateTranslation(m_currentSettings.m_worldBounds.GetCenter());
uint32_t width = aznumeric_cast<uint32_t>(
(float)m_currentSettings.m_worldBounds.GetXExtent() / m_currentSettings.m_heightQueryResolution.GetX());
uint32_t height = aznumeric_cast<uint32_t>(
(float)m_currentSettings.m_worldBounds.GetYExtent() / m_currentSettings.m_heightQueryResolution.GetY());
AZStd::vector<float> pixels;
pixels.resize_no_construct(width * height);
const uint32_t pixelDataSize = width * height * sizeof(float);
memset(pixels.data(), 0, pixelDataSize);
for (uint32_t y = 0; y < height; y++)
{
for (uint32_t x = 0; x < width; x++)
{
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);
pixels[(y * width) + x] =
(terrainHeight - m_currentSettings.m_worldBounds.GetMin().GetZ()) /
m_currentSettings.m_worldBounds.GetExtents().GetZ();
}
}
if (auto rpi = AZ::RPI::RPISystemInterface::Get(); rpi)
{
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);
}
}
}
}
if (terrainSettingsChanged || m_terrainHeightDirty)
{
// Block other threads from accessing the surface data bus while we are in GetValue (which may call into the SurfaceData bus).
+281 -34
View File
@@ -19,15 +19,31 @@
#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;
@@ -41,6 +57,9 @@ protected:
void TearDown() override
{
m_terrainSystem.reset();
m_boxShapeRequests.reset();
m_shapeRequests.reset();
m_terrainAreaHeightRequests.reset();
m_app.Destroy();
}
@@ -71,6 +90,53 @@ protected:
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)
@@ -122,10 +188,8 @@ TEST_F(TerrainSystemTest, TerrainDoesNotExistWhenNoTerrainLayerSpawnersAreRegist
// 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{});
// Create and activate the terrain system with our testing defaults for world bounds and query resolution.
CreateAndActivateTerrainSystem();
AZ::Aabb worldBounds = m_terrainSystem->GetTerrainAabb();
@@ -139,7 +203,7 @@ TEST_F(TerrainSystemTest, TerrainDoesNotExistWhenNoTerrainLayerSpawnersAreRegist
bool terrainExists = true;
float height = m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists);
EXPECT_FALSE(terrainExists);
EXPECT_EQ(height, worldBounds.GetMin().GetZ());
EXPECT_FLOAT_EQ(height, worldBounds.GetMin().GetZ());
terrainExists = true;
AZ::Vector3 normal = m_terrainSystem->GetNormal(
@@ -162,37 +226,21 @@ TEST_F(TerrainSystemTest, TerrainExistsOnlyWithinTerrainLayerSpawnerBounds)
// 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());
// 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 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 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.
@@ -215,7 +263,7 @@ TEST_F(TerrainSystemTest, TerrainExistsOnlyWithinTerrainLayerSpawnerBounds)
{
EXPECT_TRUE(heightQueryTerrainExists);
EXPECT_FALSE(isHole);
EXPECT_EQ(height, spawnerHeight);
EXPECT_FLOAT_EQ(height, spawnerHeight);
}
else
{
@@ -226,3 +274,202 @@ TEST_F(TerrainSystemTest, TerrainExistsOnlyWithinTerrainLayerSpawnerBounds)
}
}
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);
}
}
@@ -15,6 +15,8 @@ set(FILES
Source/EditorComponents/EditorTerrainWorldComponent.h
Source/EditorComponents/EditorTerrainWorldDebuggerComponent.cpp
Source/EditorComponents/EditorTerrainWorldDebuggerComponent.h
Source/EditorComponents/EditorTerrainWorldRendererComponent.cpp
Source/EditorComponents/EditorTerrainWorldRendererComponent.h
Source/EditorComponents/EditorTerrainSystemComponent.cpp
Source/EditorComponents/EditorTerrainSystemComponent.h
Source/EditorTerrainModule.cpp
+2
View File
@@ -19,6 +19,8 @@ set(FILES
Source/Components/TerrainWorldComponent.h
Source/Components/TerrainWorldDebuggerComponent.cpp
Source/Components/TerrainWorldDebuggerComponent.h
Source/Components/TerrainWorldRendererComponent.cpp
Source/Components/TerrainWorldRendererComponent.h
Source/TerrainRenderer/TerrainFeatureProcessor.cpp
Source/TerrainRenderer/TerrainFeatureProcessor.h
Source/TerrainSystem/TerrainSystem.cpp