Terrain/sphrose/layer spawner (#3980)

* #3326 Get layer priorities to work.

Signed-off-by: sphrose <82213493+sphrose@users.noreply.github.com>

* Refresh terrain when layer settings change

Signed-off-by: sphrose <82213493+sphrose@users.noreply.github.com>

* Removed dependency notification handling: it isn't needed due to deactivate/activate cycle caused by editor redrawing.
Moved layer registering to ordered map sorted by priority.

Signed-off-by: sphrose <82213493+sphrose@users.noreply.github.com>

* Remove unused code, add extra sort condition.

Signed-off-by: sphrose <82213493+sphrose@users.noreply.github.com>

* Fix copy/paste error.

Signed-off-by: sphrose <82213493+sphrose@users.noreply.github.com>

* Change erase method.

Signed-off-by: sphrose <82213493+sphrose@users.noreply.github.com>

* Review suggestions.

Signed-off-by: sphrose <82213493+sphrose@users.noreply.github.com>

* Fix bus disconnect order, change GetUseGroundPlane to return bool.

Signed-off-by: sphrose <82213493+sphrose@users.noreply.github.com>

* Create unit tests for Terrain Spawning component #3224

Signed-off-by: sphrose <82213493+sphrose@users.noreply.github.com>

* Remove unintended commit.

Signed-off-by: sphrose <82213493+sphrose@users.noreply.github.com>

* Remove blank line.

Signed-off-by: sphrose <82213493+sphrose@users.noreply.github.com>

* PR changes.

Signed-off-by: sphrose <82213493+sphrose@users.noreply.github.com>
This commit is contained in:
sphrose
2021-09-08 16:55:22 +01:00
committed by GitHub
parent 817f8ce4c1
commit cc7cc9b7a8
8 changed files with 451 additions and 43 deletions
@@ -105,17 +105,19 @@ namespace Terrain
AZ::TransformNotificationBus::Handler::BusConnect(GetEntityId());
LmbrCentral::ShapeComponentNotificationsBus::Handler::BusConnect(GetEntityId());
TerrainAreaRequestBus::Handler::BusConnect(GetEntityId());
TerrainSpawnerRequestBus::Handler::BusConnect(GetEntityId());
TerrainSystemServiceRequestBus::Broadcast(&TerrainSystemServiceRequestBus::Events::RegisterArea, GetEntityId());
}
void TerrainLayerSpawnerComponent::Deactivate()
{
TerrainAreaRequestBus::Handler::BusDisconnect();
TerrainSystemServiceRequestBus::Broadcast(&TerrainSystemServiceRequestBus::Events::UnregisterArea, GetEntityId());
AZ::TransformNotificationBus::Handler::BusDisconnect();
TerrainSpawnerRequestBus::Handler::BusDisconnect();
TerrainAreaRequestBus::Handler::BusDisconnect();
LmbrCentral::ShapeComponentNotificationsBus::Handler::BusDisconnect();
AZ::TransformNotificationBus::Handler::BusDisconnect();
}
bool TerrainLayerSpawnerComponent::ReadInConfig(const AZ::ComponentConfig* baseConfig)
@@ -147,6 +149,17 @@ namespace Terrain
{
RefreshArea();
}
void TerrainLayerSpawnerComponent::GetPriority(AZ::u32& outLayer, AZ::u32& outPriority)
{
outLayer = m_configuration.m_layer;
outPriority = m_configuration.m_priority;
}
bool TerrainLayerSpawnerComponent::GetUseGroundPlane()
{
return m_configuration.m_useGroundPlane;
}
void TerrainLayerSpawnerComponent::RegisterArea()
{
@@ -59,6 +59,7 @@ namespace Terrain
, private AZ::TransformNotificationBus::Handler
, private LmbrCentral::ShapeComponentNotificationsBus::Handler
, private Terrain::TerrainAreaRequestBus::Handler
, private Terrain::TerrainSpawnerRequestBus::Handler
{
public:
template<typename, typename>
@@ -80,7 +81,6 @@ namespace Terrain
bool ReadInConfig(const AZ::ComponentConfig* baseConfig) override;
bool WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const override;
//////////////////////////////////////////////////////////////////////////
// AZ::TransformNotificationBus::Handler
void OnTransformChanged(const AZ::Transform& local, const AZ::Transform& world) override;
@@ -88,6 +88,10 @@ namespace Terrain
// ShapeComponentNotificationsBus
void OnShapeChanged(ShapeChangeReasons changeReason) override;
// TerrainSpawnerRequestBus
void GetPriority(AZ::u32& outLayer, AZ::u32& outPriority) override;
bool GetUseGroundPlane() override;
void RegisterArea() override;
void RefreshArea() override;
@@ -17,6 +17,33 @@
using namespace Terrain;
bool TerrainLayerPriorityComparator::operator()(const AZ::EntityId& layer1id, const AZ::EntityId& layer2id) const
{
// Comparator for insertion/keylookup.
// Sorts into layer/priority order, highest priority first.
AZ::u32 priority1, layer1;
Terrain::TerrainSpawnerRequestBus::Event(layer1id, &Terrain::TerrainSpawnerRequestBus::Events::GetPriority, layer1, priority1);
AZ::u32 priority2, layer2;
Terrain::TerrainSpawnerRequestBus::Event(layer2id, &Terrain::TerrainSpawnerRequestBus::Events::GetPriority, layer2, priority2);
if (layer1 < layer2)
{
return false;
}
else if (layer1 > layer2)
{
return true;
}
if (priority1 != priority2)
{
return priority1 > priority2;
}
return layer1id > layer2id;
}
TerrainSystem::TerrainSystem()
{
Terrain::TerrainSystemServiceRequestBus::Handler::BusConnect();
@@ -78,17 +105,14 @@ float TerrainSystem::GetHeightSynchronous(float x, float y) const
AZStd::shared_lock<AZStd::shared_mutex> lock(m_areaMutex);
if (!m_registeredAreas.empty())
for (auto& [areaId, areaBounds] : m_registeredAreas)
{
for (auto& [areaId, areaBounds] : m_registeredAreas)
inPosition.SetZ(areaBounds.GetMin().GetZ());
if (areaBounds.Contains(inPosition))
{
inPosition.SetZ(areaBounds.GetMin().GetZ());
if (areaBounds.Contains(inPosition))
{
Terrain::TerrainAreaHeightRequestBus::Event(
areaId, &Terrain::TerrainAreaHeightRequestBus::Events::GetHeight, inPosition, outPosition,
Terrain::TerrainAreaHeightRequestBus::Events::Sampler::DEFAULT);
}
Terrain::TerrainAreaHeightRequestBus::Event(
areaId, &Terrain::TerrainAreaHeightRequestBus::Events::GetHeight, inPosition, outPosition,
Terrain::TerrainAreaHeightRequestBus::Events::Sampler::DEFAULT);
}
}
@@ -305,26 +329,34 @@ void TerrainSystem::SystemDeactivate()
void TerrainSystem::RegisterArea(AZ::EntityId areaId)
{
{
AZStd::unique_lock<AZStd::shared_mutex> lock(m_areaMutex);
AZ::Aabb aabb = AZ::Aabb::CreateNull();
LmbrCentral::ShapeComponentRequestsBus::EventResult(aabb, areaId, &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb);
m_registeredAreas[areaId] = aabb;
}
RefreshArea(areaId);
AZStd::unique_lock<AZStd::shared_mutex> lock(m_areaMutex);
AZ::Aabb aabb = AZ::Aabb::CreateNull();
LmbrCentral::ShapeComponentRequestsBus::EventResult(aabb, areaId, &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb);
m_registeredAreas[areaId] = aabb;
m_dirtyRegion.AddAabb(aabb);
m_terrainHeightDirty = true;
}
void TerrainSystem::UnregisterArea(AZ::EntityId areaId)
{
{
AZStd::unique_lock<AZStd::shared_mutex> lock(m_areaMutex);
AZ::Aabb aabb = AZ::Aabb::CreateNull();
LmbrCentral::ShapeComponentRequestsBus::EventResult(aabb, areaId, &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb);
m_registeredAreas.erase(areaId);
}
AZStd::unique_lock<AZStd::shared_mutex> lock(m_areaMutex);
RefreshArea(areaId);
// Remove the data for this entity from the registered areas.
// Erase_if is used as erase would use the comparator to lookup the entity id in the map.
// As the comparator will get the new layer/priority data for the entity, the id lookup will fail.
AZStd::erase_if(
m_registeredAreas,
[areaId, this](const auto& item)
{
auto const& [entityId, aabb] = item;
if (areaId == entityId)
{
m_dirtyRegion.AddAabb(aabb);
m_terrainHeightDirty = true;
return true;
}
return false;
});
}
void TerrainSystem::RefreshArea(AZ::EntityId areaId)
@@ -336,7 +368,6 @@ void TerrainSystem::RefreshArea(AZ::EntityId areaId)
AZ::Aabb oldAabb = (areaAabb != m_registeredAreas.end()) ? areaAabb->second : AZ::Aabb::CreateNull();
AZ::Aabb newAabb = AZ::Aabb::CreateNull();
LmbrCentral::ShapeComponentRequestsBus::EventResult(newAabb, areaId, &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb);
m_registeredAreas[areaId] = newAabb;
AZ::Aabb expandedAabb = oldAabb;
@@ -400,31 +431,37 @@ void TerrainSystem::OnTick(float /*deltaTime*/, AZ::ScriptTimePoint /*time*/)
const uint32_t pixelDataSize = width * height * sizeof(float);
memset(pixels.data(), 0, pixelDataSize);
for (auto& [areaId, areaBounds] : m_registeredAreas)
for (uint32_t y = 0; y < height; y++)
{
for (uint32_t y = 0; y < height; y++)
for (uint32_t x = 0; x < width; x++)
{
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());
if (areaBounds.Contains(inPosition))
if (!areaBounds.Contains(inPosition))
{
AZ::Vector3 outPosition;
const Terrain::TerrainAreaHeightRequests::Sampler sampleFilter =
Terrain::TerrainAreaHeightRequests::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();
continue;
}
AZ::Vector3 outPosition;
const Terrain::TerrainAreaHeightRequests::Sampler sampleFilter = Terrain::TerrainAreaHeightRequests::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;
}
}
}
const AZ::RPI::Scene* scene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene().get();
auto terrainFeatureProcessor = scene->GetFeatureProcessor<TerrainFeatureProcessor>();
@@ -25,6 +25,11 @@
namespace Terrain
{
struct TerrainLayerPriorityComparator
{
bool operator()(const AZ::EntityId& layer1id, const AZ::EntityId& layer2id) const;
};
class TerrainSystem
: public AzFramework::Terrain::TerrainDataRequestBus::Handler
, private Terrain::TerrainSystemServiceRequestBus::Handler
@@ -112,6 +117,6 @@ namespace Terrain
AZ::Aabb m_dirtyRegion;
mutable AZStd::shared_mutex m_areaMutex;
AZStd::unordered_map<AZ::EntityId, AZ::Aabb> m_registeredAreas;
AZStd::map<AZ::EntityId, AZ::Aabb, TerrainLayerPriorityComparator> m_registeredAreas;
};
} // namespace Terrain
@@ -112,5 +112,26 @@ namespace Terrain
};
using TerrainAreaHeightRequestBus = AZ::EBus<TerrainAreaHeightRequests>;
/**
* A bus for the TerrainSystem to interrogate TerrainLayerSpawners.
*/
class TerrainSpawnerRequests
: public AZ::ComponentBus
{
public:
////////////////////////////////////////////////////////////////////////
// EBusTraits
using MutexType = AZStd::recursive_mutex;
////////////////////////////////////////////////////////////////////////
virtual ~TerrainSpawnerRequests() = default;
virtual void GetPriority(AZ::u32& outLayer, AZ::u32& outPriority) = 0;
virtual bool GetUseGroundPlane() = 0;
};
using TerrainSpawnerRequestBus = AZ::EBus<TerrainSpawnerRequests>;
}
@@ -0,0 +1,222 @@
/*
* 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 <AzFramework/Terrain/TerrainDataRequestBus.h>
#include <Components/TerrainLayerSpawnerComponent.h>
#include <LmbrCentral/Shape/BoxShapeComponentBus.h>
#include <AzTest/AzTest.h>
#include <TerrainMocks.h>
class LayerSpawnerComponentTest
: public ::testing::Test
{
protected:
AZ::ComponentApplication m_app;
AZStd::unique_ptr<AZ::Entity> m_entity;
Terrain::TerrainLayerSpawnerComponent* m_layerSpawnerComponent;
UnitTest::MockBoxShapeComponent* m_shapeComponent;
AZStd::unique_ptr<UnitTest::MockTerrainSystem> m_terrainSystem;
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
{
if (m_terrainSystem)
{
m_terrainSystem->Deactivate();
}
m_app.Destroy();
}
void CreateEntity()
{
m_entity = AZStd::make_unique<AZ::Entity>();
m_entity->Init();
ASSERT_TRUE(m_entity);
}
void AddLayerSpawnerAndShapeComponentToEntity()
{
AddLayerSpawnerAndShapeComponentToEntity(Terrain::TerrainLayerSpawnerConfig());
}
void AddLayerSpawnerAndShapeComponentToEntity(const Terrain::TerrainLayerSpawnerConfig& config)
{
m_layerSpawnerComponent = m_entity->CreateComponent<Terrain::TerrainLayerSpawnerComponent>(config);
m_app.RegisterComponentDescriptor(m_layerSpawnerComponent->CreateDescriptor());
m_shapeComponent = m_entity->CreateComponent<UnitTest::MockBoxShapeComponent>();
m_app.RegisterComponentDescriptor(m_shapeComponent->CreateDescriptor());
ASSERT_TRUE(m_layerSpawnerComponent);
ASSERT_TRUE(m_shapeComponent);
}
void ResetEntity()
{
m_entity->Deactivate();
m_entity->Reset();
}
void CreateMockTerrainSystem()
{
m_terrainSystem = AZStd::make_unique<UnitTest::MockTerrainSystem>();
m_terrainSystem->Activate();
}
};
TEST_F(LayerSpawnerComponentTest, ActivatEntityActivateSuccess)
{
CreateEntity();
AddLayerSpawnerAndShapeComponentToEntity();
m_entity->Activate();
EXPECT_EQ(m_entity->GetState(), AZ::Entity::State::Active);
ResetEntity();
}
TEST_F(LayerSpawnerComponentTest, LayerSpawnerDefaultValuesCorrect)
{
CreateEntity();
AddLayerSpawnerAndShapeComponentToEntity();
m_entity->Activate();
AZ::u32 priority = 999, layer = 999;
Terrain::TerrainSpawnerRequestBus::Event(m_entity->GetId(), &Terrain::TerrainSpawnerRequestBus::Events::GetPriority, layer, priority);
EXPECT_EQ(0, priority);
EXPECT_EQ(1, layer);
bool useGroundPlane = false;
Terrain::TerrainSpawnerRequestBus::EventResult(useGroundPlane, m_entity->GetId(), &Terrain::TerrainSpawnerRequestBus::Events::GetUseGroundPlane);
EXPECT_TRUE(useGroundPlane);
ResetEntity();
}
TEST_F(LayerSpawnerComponentTest, LayerSpawnerConfigValuesCorrect)
{
CreateEntity();
constexpr static AZ::u32 testPriority = 15;
constexpr static AZ::u32 testLayer = 0;
Terrain::TerrainLayerSpawnerConfig config;
config.m_layer = testLayer;
config.m_priority = testPriority;
config.m_useGroundPlane = false;
AddLayerSpawnerAndShapeComponentToEntity(config);
m_entity->Activate();
AZ::u32 priority = 999, layer = 999;
Terrain::TerrainSpawnerRequestBus::Event(m_entity->GetId(), &Terrain::TerrainSpawnerRequestBus::Events::GetPriority, layer, priority);
EXPECT_EQ(testPriority, priority);
EXPECT_EQ(testLayer, layer);
bool useGroundPlane = true;
Terrain::TerrainSpawnerRequestBus::EventResult(
useGroundPlane, m_entity->GetId(), &Terrain::TerrainSpawnerRequestBus::Events::GetUseGroundPlane);
EXPECT_FALSE(useGroundPlane);
ResetEntity();
}
TEST_F(LayerSpawnerComponentTest, LayerSpawnerRegisterAreaUpdatesTerrainSystem)
{
CreateEntity();
CreateMockTerrainSystem();
AddLayerSpawnerAndShapeComponentToEntity();
m_entity->Activate();
// The Activate call should have registered the area.
EXPECT_EQ(1, m_terrainSystem->m_registerAreaCalledCount);
ResetEntity();
}
TEST_F(LayerSpawnerComponentTest, LayerSpawnerUnregisterAreaUpdatesTerrainSystem)
{
CreateEntity();
CreateMockTerrainSystem();
AddLayerSpawnerAndShapeComponentToEntity();
m_entity->Activate();
m_layerSpawnerComponent->Deactivate();
// The Deactivate call should have unregistered the area.
EXPECT_EQ(1, m_terrainSystem->m_unregisterAreaCalledCount);
ResetEntity();
}
TEST_F(LayerSpawnerComponentTest, LayerSpawnerTransformChangedUpdatesTerrainSystem)
{
CreateEntity();
CreateMockTerrainSystem();
AddLayerSpawnerAndShapeComponentToEntity();
m_entity->Activate();
AZ::TransformNotificationBus::Event(
m_entity->GetId(), &AZ::TransformNotificationBus::Events::OnTransformChanged, AZ::Transform(), AZ::Transform());
EXPECT_EQ(1, m_terrainSystem->m_refreshAreaCalledCount);
ResetEntity();
}
TEST_F(LayerSpawnerComponentTest, LayerSpawnerShapeChangedUpdatesTerrainSystem)
{
CreateEntity();
CreateMockTerrainSystem();
AddLayerSpawnerAndShapeComponentToEntity();
m_entity->Activate();
LmbrCentral::ShapeComponentNotificationsBus::Event(
m_entity->GetId(), &LmbrCentral::ShapeComponentNotificationsBus::Events::OnShapeChanged,
LmbrCentral::ShapeComponentNotifications::ShapeChangeReasons::ShapeChanged);
EXPECT_EQ(1, m_terrainSystem->m_refreshAreaCalledCount);
ResetEntity();
}
+104
View File
@@ -0,0 +1,104 @@
/*
* 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/ComponentApplication.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));
}
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 MockTerrainSystem : private Terrain::TerrainSystemServiceRequestBus::Handler
{
public:
void Activate() override
{
Terrain::TerrainSystemServiceRequestBus::Handler::BusConnect();
}
void Deactivate() override
{
Terrain::TerrainSystemServiceRequestBus::Handler::BusDisconnect();
}
void SetWorldBounds(const AZ::Aabb& worldBounds) override
{
}
void SetHeightQueryResolution([[maybe_unused]] AZ::Vector2 queryResolution) override
{
}
void RegisterArea([[maybe_unused]] AZ::EntityId areaId) override
{
m_registerAreaCalledCount++;
}
void UnregisterArea([[maybe_unused]] AZ::EntityId areaId) override
{
m_unregisterAreaCalledCount++;
}
void RefreshArea([[maybe_unused]] AZ::EntityId areaId) override
{
m_refreshAreaCalledCount++;
}
int m_registerAreaCalledCount = 0;
int m_refreshAreaCalledCount = 0;
int m_unregisterAreaCalledCount = 0;
};
}
@@ -7,5 +7,7 @@
#
set(FILES
Tests/TerrainMocks.h
Tests/TerrainTest.cpp
Tests/LayerSpawnerTests.cpp
)