56904d1799
* Generic Cleanup Removals: * Unused cvars,member variables and defines * Windows media center edition support routines * CSystem - removed debug_GetCallStackRaw/GetRootWindowMessageHandler/ UnloadDLL/ShutdownModuleLibraries Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> * Fix CryMessageBox return values in Windows build Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> * Remove named selection group and a few smaller unused functionalities Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> * Remove ObjectManager export functionality + 2 deprecated functions Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> * Remove object legacy freeze/hide support from ObjectManager Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> * Remove ObjectManager duplicate name dection, as well as object renaming Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> * Remove ObjectManager serialization and selection callbacks Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> * ObjectManager - remove MoveObjects/HitTestObject/EndEditParams and related members/functions Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> * Remove a bunch of unused clone related functionality + misc Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> * ObjectManager - misc removals Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> * ObjectManager - more removals Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> * Remove unused object selection/retrieval in ObjectManager and LoadObjects in ObjectLoader Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> * CBaseObject - remove unused material layers mask support Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> * More CBaseObject cleanups. Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> * CBaseObject - remove SubObj functions and IMouseCreateCallback Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> * Remove unused procedural floor management, helper scale and tags. Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> * CBaseObject - remove more unused methods. Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> * Removals in multiple places. CBaseObject - unused `OBJTYPE_DUMMY` flag removed, member `IsSameClass` CObjectArchive removed unused methods/members CObjectManager removed unused `Update` method Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> * More removals IDisplayViewport - HitTestLine/GetGridStep/setHitcontext/ GetConstructionPlane Unused Cry_Matrix44 template specializations. Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> * O3DE.exe Project-Centric "Open Editor" fix (#5852) * The O3DE.exe Open Editor button now attempts to open the Editor in the build directory of the project being opened. If their is no Editor within the build directory of the Project, it uses the Editor.exe in the current O3DE.exe executable directory if it exists Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> * Engine .gitignore now ignores the build directory if placed in the AutomatedTesting project Previously it was just ignoring a `[Bb]uild` directory if it was directly within the engine root. This change matches the behavior of the project templates. Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> * Renamed the ProjectUtils GetEditorDirectory function to GetEditorExecutablePath Added a platform specific implementation for retrieving the path to the Editor executable in the GetEditorExectuablePath function. It first attempts to locate the Editor via checking the project build directory for an Editor executable before falling back to checking the binary directory of the currently running O3DE executable. Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> * Correct the MacOS GetEditorExecutablePath to return the Editor path Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> * Adding missing C++20 std::erase implementations (#5735) There were already implementations for std::erase_if. This adds the counterpart AZStd::erase versions resolves #5734 Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> * More removals and refactors `ShowMessage` and `CryMessageBox` return void now Simplify code in `CSystem::WarningV` Remove unused `CryGetTicksPerSec` Remove unused WinBase functionality Replace `nanosleep` call in WinBase with `std::this_thread::sleep_for` Remove unused Win32Wrapper routines Remove unused IFunctorBase.h and IEntityObjectListener.h Fix VectorAndArray.cpp compilation Use QMessageBox instead of CryMessageBox in the editor. Remove empty ArchiveVars platform specific files Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> * Fix test code. Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> * Remove an unused function Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> * CBaseObject and undo description removals. Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> * A bunch of removals * Remove IRenderNode * Remove editor's KDTree * Remove StatObjBus, InstanceStatObjEventBus::ReleaseData is redundant ( same functionality in the only available handler is triggered by `OnCryEditorCloseScene`) * Remove CExportManager::AddStatObj/AddMeshes/AddMesh, IExportManager::ExportSingleStatObj * Remove CIconManager/IIconManager::GetObject * Remove CBaseObject::IntersectRayMesh * Remove IIndexedMesh and related structs. * Unused IUndoObject::GetDescription and all derived implementations. * Unused CUndoBaseLibrary/CUndoBaseLibraryManager * Unused Matrix34_tpl typedefs * Legacy Xml classes cleanup Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> * Remove unused `Vec3ToVector3D` helper Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> * fix - remove unused static variable Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> Co-authored-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com>
1834 lines
88 KiB
C++
1834 lines
88 KiB
C++
/*
|
|
* Copyright (c) Contributors to the Open 3D Engine Project.
|
|
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
*
|
|
*/
|
|
|
|
#include <VegetationProfiler.h>
|
|
#include "AreaSystemComponent.h"
|
|
|
|
#include <Vegetation/Ebuses/AreaNotificationBus.h>
|
|
#include <Vegetation/Ebuses/AreaRequestBus.h>
|
|
#include <Vegetation/Ebuses/InstanceSystemRequestBus.h>
|
|
#include <Vegetation/Ebuses/AreaInfoBus.h>
|
|
#include <Vegetation/Ebuses/DebugNotificationBus.h>
|
|
#include <Vegetation/Ebuses/DebugSystemDataBus.h>
|
|
#include <SurfaceData/SurfaceDataSystemRequestBus.h>
|
|
#include <SurfaceData/Utility/SurfaceDataUtility.h>
|
|
|
|
#include <AzCore/Debug/Profiler.h>
|
|
#include <AzCore/RTTI/BehaviorContext.h>
|
|
#include <AzCore/Serialization/EditContext.h>
|
|
#include <AzCore/Serialization/SerializeContext.h>
|
|
#include <AzCore/Jobs/JobFunction.h>
|
|
#include <AzCore/std/chrono/chrono.h>
|
|
#include <AzCore/std/sort.h>
|
|
#include <AzCore/std/utils.h>
|
|
#include <AzCore/Component/TransformBus.h>
|
|
|
|
|
|
#include <AzFramework/Components/CameraBus.h>
|
|
#ifdef VEGETATION_EDITOR
|
|
#include <AzToolsFramework/API/EditorCameraBus.h>
|
|
#endif
|
|
|
|
#include <ISystem.h>
|
|
#include <cinttypes>
|
|
|
|
namespace Vegetation
|
|
{
|
|
namespace AreaSystemUtil
|
|
{
|
|
template <typename T>
|
|
void hash_combine_64(uint64_t& seed, T const& v)
|
|
{
|
|
AZStd::hash<T> hasher;
|
|
seed ^= hasher(v) + 0x9e3779b97f4a7c13LL + (seed << 12) + (seed >> 4);
|
|
}
|
|
|
|
static bool UpdateVersion([[maybe_unused]] AZ::SerializeContext& context, AZ::SerializeContext::DataElementNode& classElement)
|
|
{
|
|
if (classElement.GetVersion() < 4)
|
|
{
|
|
classElement.RemoveElementByName(AZ_CRC("ThreadSleepTimeMs", 0x9e86f79d));
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// ViewRect
|
|
|
|
inline bool AreaSystemComponent::ViewRect::IsInside(const SectorId& sector) const
|
|
{
|
|
const int inX = sector.first;
|
|
const int inY = sector.second;
|
|
return inX >= GetMinXSector() && inX <= GetMaxXSector() && inY >= GetMinYSector() && inY <= GetMaxYSector();
|
|
}
|
|
|
|
AreaSystemComponent::ViewRect AreaSystemComponent::ViewRect::Overlap(const ViewRect& b) const
|
|
{
|
|
ViewRect o;
|
|
o.m_x = m_x > b.m_x ? m_x : b.m_x;
|
|
o.m_y = m_y > b.m_y ? m_y : b.m_y;
|
|
o.m_width = m_x + m_width > b.m_x + b.m_width ? b.m_x + b.m_width : m_x + m_width;
|
|
o.m_height = m_y + m_height > b.m_y + b.m_height ? b.m_y + b.m_height : m_y + m_height;
|
|
o.m_width -= o.m_x;
|
|
o.m_height -= o.m_y;
|
|
return o;
|
|
}
|
|
|
|
bool AreaSystemComponent::ViewRect::operator==(const ViewRect& b)
|
|
{
|
|
return m_x == b.m_x && m_y == b.m_y && m_width == b.m_width && m_height == b.m_height;
|
|
}
|
|
|
|
size_t AreaSystemComponent::ViewRect::GetNumSectors() const
|
|
{
|
|
return static_cast<size_t>(m_height * m_width);
|
|
}
|
|
|
|
bool AreaSystemComponent::ViewRect::operator!=(const ViewRect& b)
|
|
{
|
|
return m_x != b.m_x || m_y != b.m_y || m_width != b.m_width || m_height != b.m_height;
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// DirtySectors
|
|
void AreaSystemComponent::DirtySectors::MarkDirty(const SectorId& sector)
|
|
{
|
|
m_dirtySet.insert(AZStd::move(sector));
|
|
}
|
|
|
|
void AreaSystemComponent::DirtySectors::MarkAllDirty()
|
|
{
|
|
m_allSectorsDirty = true;
|
|
}
|
|
|
|
void AreaSystemComponent::DirtySectors::Clear()
|
|
{
|
|
m_dirtySet.clear();
|
|
m_allSectorsDirty = false;
|
|
}
|
|
|
|
bool AreaSystemComponent::DirtySectors::IsDirty(const SectorId& sector) const
|
|
{
|
|
return m_allSectorsDirty ||
|
|
(!m_dirtySet.empty() && (m_dirtySet.find(sector) != m_dirtySet.end()));
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// AreaSystemConfig
|
|
|
|
// These limitations are somewhat arbitrary. It's possible to select combinations of larger values than these that will work successfully.
|
|
// However, these values are also large enough that going beyond them is extremely likely to cause problems.
|
|
const int AreaSystemConfig::s_maxViewRectangleSize = 128;
|
|
const int AreaSystemConfig::s_maxSectorDensity = 64;
|
|
const int AreaSystemConfig::s_maxSectorSizeInMeters = 1024;
|
|
const int64_t AreaSystemConfig::s_maxVegetationInstances = 2 * 1024 * 1024;
|
|
const int AreaSystemConfig::s_maxInstancesPerMeter = 16;
|
|
|
|
void AreaSystemConfig::Reflect(AZ::ReflectContext* context)
|
|
{
|
|
AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context);
|
|
if (serialize)
|
|
{
|
|
serialize->Class<AreaSystemConfig, AZ::ComponentConfig>()
|
|
->Version(4, &AreaSystemUtil::UpdateVersion)
|
|
->Field("ViewRectangleSize", &AreaSystemConfig::m_viewRectangleSize)
|
|
->Field("SectorDensity", &AreaSystemConfig::m_sectorDensity)
|
|
->Field("SectorSizeInMeters", &AreaSystemConfig::m_sectorSizeInMeters)
|
|
->Field("ThreadProcessingIntervalMs", &AreaSystemConfig::m_threadProcessingIntervalMs)
|
|
->Field("SectorSearchPadding", &AreaSystemConfig::m_sectorSearchPadding)
|
|
->Field("SectorPointSnapMode", &AreaSystemConfig::m_sectorPointSnapMode)
|
|
;
|
|
|
|
AZ::EditContext* edit = serialize->GetEditContext();
|
|
if (edit)
|
|
{
|
|
edit->Class<AreaSystemConfig>(
|
|
"Vegetation Area System Config", "Handles the placement and removal of vegetation instance based on the vegetation area component rules")
|
|
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
|
|
->Attribute(AZ::Edit::Attributes::AutoExpand, true)
|
|
->DataElement(AZ::Edit::UIHandlers::Default, &AreaSystemConfig::m_viewRectangleSize, "View Area Grid Size", "The number of sectors (per-side) of a managed grid in a scrolling view centered around the camera.")
|
|
->Attribute(AZ::Edit::Attributes::ChangeValidate, &AreaSystemConfig::ValidateViewArea)
|
|
->Attribute(AZ::Edit::Attributes::Min, 1)
|
|
->Attribute(AZ::Edit::Attributes::Max, s_maxViewRectangleSize)
|
|
->DataElement(AZ::Edit::UIHandlers::Default, &AreaSystemConfig::m_sectorDensity, "Sector Point Density", "The number of equally-spaced vegetation instance grid placement points (per-side) within a sector")
|
|
->Attribute(AZ::Edit::Attributes::ChangeValidate, &AreaSystemConfig::ValidateSectorDensity)
|
|
->Attribute(AZ::Edit::Attributes::Min, 1)
|
|
->Attribute(AZ::Edit::Attributes::Max, s_maxSectorDensity)
|
|
->DataElement(AZ::Edit::UIHandlers::Default, &AreaSystemConfig::m_sectorSizeInMeters, "Sector Size In Meters", "The size in meters (per-side) of each sector.")
|
|
->Attribute(AZ::Edit::Attributes::ChangeValidate, &AreaSystemConfig::ValidateSectorSize)
|
|
->Attribute(AZ::Edit::Attributes::Min, 1)
|
|
->Attribute(AZ::Edit::Attributes::Max, s_maxSectorSizeInMeters)
|
|
->DataElement(AZ::Edit::UIHandlers::Default, &AreaSystemConfig::m_threadProcessingIntervalMs, "Thread Processing Interval", "The delay (in milliseconds) between processing queued thread tasks.")
|
|
->Attribute(AZ::Edit::Attributes::Min, 0)
|
|
->Attribute(AZ::Edit::Attributes::Max, 5000)
|
|
->DataElement(AZ::Edit::UIHandlers::Default, &AreaSystemConfig::m_sectorSearchPadding, "Sector Search Padding", "Increases the search radius for surrounding sectors when enumerating instances.")
|
|
->Attribute(AZ::Edit::Attributes::Min, 0)
|
|
->Attribute(AZ::Edit::Attributes::Max, 2)
|
|
->DataElement(AZ::Edit::UIHandlers::ComboBox, &AreaSystemConfig::m_sectorPointSnapMode, "Sector Point Snap Mode", "Controls whether vegetation placement points are located at the corner or the center of the cell.")
|
|
->EnumAttribute(SnapMode::Corner, "Corner")
|
|
->EnumAttribute(SnapMode::Center, "Center")
|
|
;
|
|
}
|
|
}
|
|
|
|
if (auto behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
|
|
{
|
|
behaviorContext->Class<AreaSystemConfig>()
|
|
->Attribute(AZ::Script::Attributes::Category, "Vegetation")
|
|
->Constructor()
|
|
->Property("viewRectangleSize", BehaviorValueProperty(&AreaSystemConfig::m_viewRectangleSize))
|
|
->Property("sectorDensity", BehaviorValueProperty(&AreaSystemConfig::m_sectorDensity))
|
|
->Property("sectorSizeInMeters", BehaviorValueProperty(&AreaSystemConfig::m_sectorSizeInMeters))
|
|
->Property("threadProcessingIntervalMs", BehaviorValueProperty(&AreaSystemConfig::m_threadProcessingIntervalMs))
|
|
->Property("sectorPointSnapMode",
|
|
[](AreaSystemConfig* config) { return static_cast<AZ::u8>(config->m_sectorPointSnapMode); },
|
|
[](AreaSystemConfig* config, const AZ::u8& i) { config->m_sectorPointSnapMode = static_cast<SnapMode>(i); })
|
|
;
|
|
}
|
|
}
|
|
|
|
AZ::Outcome<void, AZStd::string> AreaSystemConfig::ValidateViewArea(void* newValue, const AZ::Uuid& valueType)
|
|
{
|
|
if (azrtti_typeid<int>() != valueType)
|
|
{
|
|
AZ_Assert(false, "Unexpected value type");
|
|
return AZ::Failure(AZStd::string("Unexpectedly received a non-int type for the View Area Grid Size!"));
|
|
}
|
|
|
|
int viewRectangleSize = *static_cast<int*>(newValue);
|
|
const int instancesPerSector = m_sectorDensity * m_sectorDensity;
|
|
const int totalSectors = viewRectangleSize * viewRectangleSize;
|
|
|
|
int64_t totalInstances = instancesPerSector * totalSectors;
|
|
|
|
if (totalInstances > s_maxVegetationInstances)
|
|
{
|
|
return AZ::Failure(
|
|
AZStd::string::format("The combination of View Area Grid Size and Sector Point Density will create %" PRId64 " instances. Only a max of %" PRId64 " instances is allowed.",
|
|
totalInstances, s_maxVegetationInstances));
|
|
}
|
|
|
|
return AZ::Success();
|
|
}
|
|
|
|
AZ::Outcome<void, AZStd::string> AreaSystemConfig::ValidateSectorDensity(void* newValue, const AZ::Uuid& valueType)
|
|
{
|
|
if (azrtti_typeid<int>() != valueType)
|
|
{
|
|
AZ_Assert(false, "Unexpected value type");
|
|
return AZ::Failure(AZStd::string("Unexpectedly received a non-int type for the Sector Point Density!"));
|
|
}
|
|
|
|
int sectorDensity = *static_cast<int*>(newValue);
|
|
const int instancesPerSector = sectorDensity * sectorDensity;
|
|
const int totalSectors = m_viewRectangleSize * m_viewRectangleSize;
|
|
|
|
int64_t totalInstances = instancesPerSector * totalSectors;
|
|
|
|
if (totalInstances >= s_maxVegetationInstances)
|
|
{
|
|
return AZ::Failure(
|
|
AZStd::string::format("The combination of View Area Grid Size and Sector Point Density will create %" PRId64 " instances. Only a max of %" PRId64 " instances is allowed.",
|
|
totalInstances, s_maxVegetationInstances));
|
|
}
|
|
|
|
const float instancesPerMeter = static_cast<float>(sectorDensity) / static_cast<float>(m_sectorSizeInMeters);
|
|
if (instancesPerMeter > s_maxInstancesPerMeter)
|
|
{
|
|
return AZ::Failure(AZStd::string::format("The combination of Sector Point Density and Sector Size in Meters will create %.1f instances per meter. Only a max of %d instances per meter is allowed.",
|
|
instancesPerMeter, s_maxInstancesPerMeter));
|
|
}
|
|
|
|
return AZ::Success();
|
|
}
|
|
|
|
AZ::Outcome<void, AZStd::string> AreaSystemConfig::ValidateSectorSize(void* newValue, const AZ::Uuid& valueType)
|
|
{
|
|
if (azrtti_typeid<int>() != valueType)
|
|
{
|
|
AZ_Assert(false, "Unexpected value type");
|
|
return AZ::Failure(AZStd::string("Unexpectedly received a non-int type for the Sector Size In Meters!"));
|
|
}
|
|
|
|
int sectorSizeInMeters = *static_cast<int*>(newValue);
|
|
|
|
const float instancesPerMeter = static_cast<float>(m_sectorDensity) / static_cast<float>(sectorSizeInMeters);
|
|
if (instancesPerMeter > s_maxInstancesPerMeter)
|
|
{
|
|
return AZ::Failure(AZStd::string::format("The combination of Sector Point Density and Sector Size in Meters will create %.1f instances per meter. Only a max of %d instances per meter is allowed.",
|
|
instancesPerMeter, s_maxInstancesPerMeter));
|
|
}
|
|
|
|
return AZ::Success();
|
|
}
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// AreaSystemComponent
|
|
|
|
void AreaSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services)
|
|
{
|
|
services.push_back(AZ_CRC("VegetationAreaSystemService", 0x36da2b62));
|
|
}
|
|
|
|
void AreaSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& services)
|
|
{
|
|
services.push_back(AZ_CRC("VegetationAreaSystemService", 0x36da2b62));
|
|
}
|
|
|
|
void AreaSystemComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& services)
|
|
{
|
|
services.push_back(AZ_CRC("VegetationDebugSystemService", 0x8cac3d67));
|
|
services.push_back(AZ_CRC("VegetationInstanceSystemService", 0x823a6007));
|
|
services.push_back(AZ_CRC("SurfaceDataSystemService", 0x1d44d25f));
|
|
}
|
|
|
|
void AreaSystemComponent::Reflect(AZ::ReflectContext* context)
|
|
{
|
|
InstanceData::Reflect(context);
|
|
|
|
AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context);
|
|
if (serialize)
|
|
{
|
|
serialize->Class<AreaSystemComponent, AZ::Component>()
|
|
->Version(0)
|
|
->Field("Configuration", &AreaSystemComponent::m_configuration)
|
|
;
|
|
|
|
if (AZ::EditContext* editContext = serialize->GetEditContext())
|
|
{
|
|
editContext->Class<AreaSystemComponent>("Vegetation Area System", "Manages registration and processing of vegetation area entities")
|
|
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
|
|
->Attribute(AZ::Edit::Attributes::Category, "Vegetation")
|
|
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("System", 0xc94d118b))
|
|
->Attribute(AZ::Edit::Attributes::AutoExpand, true)
|
|
->Attribute(AZ::Edit::Attributes::HelpPageURL, "https://o3de.org/docs/user-guide/components/reference/")
|
|
->DataElement(0, &AreaSystemComponent::m_configuration, "Configuration", "")
|
|
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
|
|
;
|
|
}
|
|
}
|
|
|
|
AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context);
|
|
if (behaviorContext)
|
|
{
|
|
behaviorContext->EBus<AreaSystemRequestBus>("AreaSystemRequestBus")
|
|
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
|
|
->Attribute(AZ::Script::Attributes::Category, "AreaSystem")
|
|
->Attribute(AZ::Script::Attributes::Module, "areasystem")
|
|
->Event("GetInstanceCountInAabb", &AreaSystemRequests::GetInstanceCountInAabb)
|
|
->Event("GetInstancesInAabb", &AreaSystemRequests::GetInstancesInAabb)
|
|
;
|
|
}
|
|
}
|
|
|
|
AreaSystemComponent::AreaSystemComponent(const AreaSystemConfig& configuration)
|
|
: m_configuration(configuration)
|
|
{
|
|
}
|
|
|
|
void AreaSystemComponent::Init()
|
|
{
|
|
}
|
|
|
|
void AreaSystemComponent::Activate()
|
|
{
|
|
// Wait for any lingering vegetation thread work to complete if necessary. (This should never actually occur)
|
|
AZ_Assert(m_threadData.m_vegetationThreadState == PersistentThreadData::VegetationThreadState::Stopped,
|
|
"Vegetation thread was still active even though AreaSystemComponent was deactivated.");
|
|
AZStd::lock_guard<decltype(m_threadData.m_vegetationThreadMutex)> lockTasks(m_threadData.m_vegetationThreadMutex);
|
|
m_threadData.m_vegetationThreadState = PersistentThreadData::VegetationThreadState::Stopped;
|
|
m_threadData.m_vegetationDataSyncState = PersistentThreadData::VegetationDataSyncState::Synchronized;
|
|
|
|
m_system = GetISystem();
|
|
m_worldToSector = 1.0f / m_configuration.m_sectorSizeInMeters;
|
|
|
|
// We initialize our vegetation threadData state here to ensure it gets recalculated the next time the thread runs.
|
|
m_threadData.Init();
|
|
|
|
AZ::TickBus::Handler::BusConnect();
|
|
AreaSystemRequestBus::Handler::BusConnect();
|
|
GradientSignal::SectorDataRequestBus::Handler::BusConnect();
|
|
SystemConfigurationRequestBus::Handler::BusConnect();
|
|
CrySystemEventBus::Handler::BusConnect();
|
|
AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusConnect();
|
|
SurfaceData::SurfaceDataSystemNotificationBus::Handler::BusConnect();
|
|
|
|
m_vegTasks.FetchDebugData();
|
|
}
|
|
|
|
void AreaSystemComponent::Deactivate()
|
|
{
|
|
// Interrupt vegetation worker; deactivation deletes all vegetation, so there's no need to process updates.
|
|
m_threadData.InterruptVegetationThread();
|
|
|
|
//wait for the vegetation thread work to complete
|
|
AZStd::lock_guard<decltype(m_threadData.m_vegetationThreadMutex)> lockTasks(m_threadData.m_vegetationThreadMutex);
|
|
m_threadData.m_vegetationThreadState = PersistentThreadData::VegetationThreadState::Stopped;
|
|
m_threadData.m_vegetationDataSyncState = PersistentThreadData::VegetationDataSyncState::Synchronized;
|
|
|
|
AZ::TickBus::Handler::BusDisconnect();
|
|
AreaSystemRequestBus::Handler::BusDisconnect();
|
|
GradientSignal::SectorDataRequestBus::Handler::BusDisconnect();
|
|
SystemConfigurationRequestBus::Handler::BusDisconnect();
|
|
CrySystemEventBus::Handler::BusDisconnect();
|
|
AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusDisconnect();
|
|
SurfaceData::SurfaceDataSystemNotificationBus::Handler::BusDisconnect();
|
|
|
|
// Clear sector data and any lingering vegetation thread state
|
|
m_vegTasks.ClearSectors();
|
|
m_threadData.Init();
|
|
|
|
InstanceSystemRequestBus::Broadcast(&InstanceSystemRequestBus::Events::DestroyAllInstances);
|
|
InstanceSystemRequestBus::Broadcast(&InstanceSystemRequestBus::Events::Cleanup);
|
|
|
|
if (m_system)
|
|
{
|
|
m_system->GetISystemEventDispatcher()->RemoveListener(this);
|
|
m_system = nullptr;
|
|
}
|
|
}
|
|
|
|
bool AreaSystemComponent::ReadInConfig(const AZ::ComponentConfig* baseConfig)
|
|
{
|
|
if (auto config = azrtti_cast<const AreaSystemConfig*>(baseConfig))
|
|
{
|
|
m_configuration = *config;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool AreaSystemComponent::WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const
|
|
{
|
|
if (auto config = azrtti_cast<AreaSystemConfig*>(outBaseConfig))
|
|
{
|
|
if (m_configDirty)
|
|
{
|
|
*config = m_pendingConfigUpdate;
|
|
}
|
|
else
|
|
{
|
|
*config = m_configuration;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void AreaSystemComponent::UpdateSystemConfig(const AZ::ComponentConfig* baseConfig)
|
|
{
|
|
if (const auto config = azrtti_cast<const AreaSystemConfig*>(baseConfig))
|
|
{
|
|
if ((!m_configDirty && m_configuration == *config) || (m_configDirty && m_pendingConfigUpdate == *config))
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_configDirty = true;
|
|
m_pendingConfigUpdate = *config;
|
|
}
|
|
}
|
|
|
|
void AreaSystemComponent::GetSystemConfig(AZ::ComponentConfig* outBaseConfig) const
|
|
{
|
|
WriteOutConfig(outBaseConfig);
|
|
}
|
|
|
|
bool AreaSystemComponent::ApplyPendingConfigChanges()
|
|
{
|
|
if (m_configDirty)
|
|
{
|
|
ReleaseWithoutCleanup();
|
|
|
|
if (m_configuration.m_threadProcessingIntervalMs != m_pendingConfigUpdate.m_threadProcessingIntervalMs)
|
|
{
|
|
m_vegetationThreadTaskTimer = 0.0f;
|
|
}
|
|
|
|
ReadInConfig(&m_pendingConfigUpdate);
|
|
m_worldToSector = 1.0f / m_configuration.m_sectorSizeInMeters;
|
|
RefreshAllAreas();
|
|
|
|
GradientSignal::SectorDataNotificationBus::Broadcast(&GradientSignal::SectorDataNotificationBus::Events::OnSectorDataConfigurationUpdated);
|
|
|
|
m_configDirty = false;
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void AreaSystemComponent::RegisterArea(AZ::EntityId areaId, AZ::u32 layer, AZ::u32 priority, const AZ::Aabb& bounds)
|
|
{
|
|
if (!bounds.IsValid())
|
|
{
|
|
AZ_Assert(false, "Vegetation Area registered with an invalid AABB.");
|
|
}
|
|
|
|
m_vegTasks.QueueVegetationTask([areaId, layer, priority, bounds](UpdateContext* context, PersistentThreadData* threadData, VegetationThreadTasks* vegTasks)
|
|
{
|
|
auto& area = threadData->m_globalVegetationAreaMap[areaId];
|
|
area.m_id = areaId;
|
|
area.m_layer = layer;
|
|
area.m_priority = priority;
|
|
area.m_bounds = bounds;
|
|
AreaNotificationBus::Event(area.m_id, &AreaNotificationBus::Events::OnAreaRegistered);
|
|
const auto& cachedMainThreadData = context->GetCachedMainThreadData();
|
|
vegTasks->MarkDirtySectors(area.m_bounds, threadData->m_dirtySectorContents,
|
|
cachedMainThreadData.m_worldToSector, cachedMainThreadData.m_currViewRect);
|
|
threadData->m_activeAreasDirty = true;
|
|
});
|
|
}
|
|
|
|
void AreaSystemComponent::UnregisterArea(AZ::EntityId areaId)
|
|
{
|
|
m_vegTasks.QueueVegetationTask([areaId](UpdateContext* context, PersistentThreadData* threadData, VegetationThreadTasks* vegTasks)
|
|
{
|
|
auto itArea = threadData->m_globalVegetationAreaMap.find(areaId);
|
|
if (itArea != threadData->m_globalVegetationAreaMap.end())
|
|
{
|
|
const auto& area = itArea->second;
|
|
AreaNotificationBus::Event(area.m_id, &AreaNotificationBus::Events::OnAreaUnregistered);
|
|
AreaNotificationBus::Event(area.m_id, &AreaNotificationBus::Events::OnAreaDisconnect);
|
|
|
|
const auto& cachedMainThreadData = context->GetCachedMainThreadData();
|
|
vegTasks->AddUnregisteredVegetationArea(area, cachedMainThreadData.m_worldToSector, cachedMainThreadData.m_currViewRect);
|
|
vegTasks->MarkDirtySectors(area.m_bounds, threadData->m_dirtySectorContents,
|
|
cachedMainThreadData.m_worldToSector, cachedMainThreadData.m_currViewRect);
|
|
threadData->m_globalVegetationAreaMap.erase(itArea);
|
|
threadData->m_activeAreasDirty = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
void AreaSystemComponent::RefreshArea(AZ::EntityId areaId, AZ::u32 layer, AZ::u32 priority, const AZ::Aabb& bounds)
|
|
{
|
|
if (!bounds.IsValid())
|
|
{
|
|
AZ_Assert(false, "Vegetation Area refreshed with an invalid AABB.");
|
|
}
|
|
|
|
m_vegTasks.QueueVegetationTask([areaId, layer, priority, bounds](UpdateContext* context, PersistentThreadData* threadData, VegetationThreadTasks* vegTasks)
|
|
{
|
|
auto itArea = threadData->m_globalVegetationAreaMap.find(areaId);
|
|
if (itArea != threadData->m_globalVegetationAreaMap.end())
|
|
{
|
|
auto& area = itArea->second;
|
|
const auto& cachedMainThreadData = context->GetCachedMainThreadData();
|
|
vegTasks->MarkDirtySectors(area.m_bounds, threadData->m_dirtySectorContents,
|
|
cachedMainThreadData.m_worldToSector, cachedMainThreadData.m_currViewRect);
|
|
|
|
area.m_layer = layer;
|
|
area.m_priority = priority;
|
|
area.m_bounds = bounds;
|
|
AreaNotificationBus::Event(area.m_id, &AreaNotificationBus::Events::OnAreaRefreshed);
|
|
|
|
vegTasks->MarkDirtySectors(area.m_bounds, threadData->m_dirtySectorContents,
|
|
cachedMainThreadData.m_worldToSector, cachedMainThreadData.m_currViewRect);
|
|
threadData->m_activeAreasDirty = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
void AreaSystemComponent::RefreshAllAreas()
|
|
{
|
|
m_vegTasks.QueueVegetationTask([](UpdateContext* context, PersistentThreadData* threadData, VegetationThreadTasks* vegTasks)
|
|
{
|
|
for (auto& entry : threadData->m_globalVegetationAreaMap)
|
|
{
|
|
auto& area = entry.second;
|
|
area.m_layer = {};
|
|
area.m_priority = {};
|
|
area.m_bounds = AZ::Aabb::CreateNull();
|
|
|
|
AreaInfoBus::EventResult(area.m_layer, area.m_id, &AreaInfoBus::Events::GetLayer);
|
|
AreaInfoBus::EventResult(area.m_priority, area.m_id, &AreaInfoBus::Events::GetPriority);
|
|
AreaInfoBus::EventResult(area.m_bounds, area.m_id, &AreaInfoBus::Events::GetEncompassingAabb);
|
|
AreaNotificationBus::Event(area.m_id, &AreaNotificationBus::Events::OnAreaRefreshed);
|
|
}
|
|
|
|
// Set all existing sectors as needing to be rebuilt.
|
|
const auto& cachedMainThreadData = context->GetCachedMainThreadData();
|
|
vegTasks->MarkDirtySectors(AZ::Aabb::CreateNull(), threadData->m_dirtySectorContents,
|
|
cachedMainThreadData.m_worldToSector, cachedMainThreadData.m_currViewRect);
|
|
vegTasks->MarkDirtySectors(AZ::Aabb::CreateNull(), threadData->m_dirtySectorSurfacePoints,
|
|
cachedMainThreadData.m_worldToSector, cachedMainThreadData.m_currViewRect);
|
|
});
|
|
}
|
|
|
|
void AreaSystemComponent::ClearAllAreas()
|
|
{
|
|
// Interrupt any work that's currently being done on the vegetation thread and destroy all vegetation instances
|
|
ReleaseWithoutCleanup();
|
|
// Queue a refresh of all the areas
|
|
RefreshAllAreas();
|
|
// Reset our timer for checking the vegetation queue for more work to ensure we process this immediately.
|
|
m_vegetationThreadTaskTimer = 0.0f;
|
|
}
|
|
|
|
void AreaSystemComponent::MuteArea(AZ::EntityId areaId)
|
|
{
|
|
m_vegTasks.QueueVegetationTask([areaId](UpdateContext* /*context*/, PersistentThreadData* threadData, VegetationThreadTasks* /*vegTasks*/)
|
|
{
|
|
threadData->m_ignoredVegetationAreaSet.insert(areaId);
|
|
threadData->m_activeAreasDirty = true;
|
|
});
|
|
}
|
|
|
|
void AreaSystemComponent::UnmuteArea(AZ::EntityId areaId)
|
|
{
|
|
m_vegTasks.QueueVegetationTask([areaId](UpdateContext* /*context*/, PersistentThreadData* threadData, VegetationThreadTasks* /*vegTasks*/)
|
|
{
|
|
threadData->m_ignoredVegetationAreaSet.erase(areaId);
|
|
threadData->m_activeAreasDirty = true;
|
|
});
|
|
}
|
|
|
|
void AreaSystemComponent::OnSurfaceChanged(const AZ::EntityId& /*entityId*/, const AZ::Aabb& oldBounds, const AZ::Aabb& newBounds)
|
|
{
|
|
m_vegTasks.QueueVegetationTask([oldBounds, newBounds](UpdateContext* context, PersistentThreadData* threadData, VegetationThreadTasks* vegTasks)
|
|
{
|
|
const auto& cachedMainThreadData = context->GetCachedMainThreadData();
|
|
|
|
// Mark the surface area prior to the surface data change as dirty
|
|
vegTasks->MarkDirtySectors(oldBounds, threadData->m_dirtySectorContents,
|
|
cachedMainThreadData.m_worldToSector, cachedMainThreadData.m_currViewRect);
|
|
vegTasks->MarkDirtySectors(oldBounds, threadData->m_dirtySectorSurfacePoints,
|
|
cachedMainThreadData.m_worldToSector, cachedMainThreadData.m_currViewRect);
|
|
|
|
// Mark the surface area *after* the surface data change as dirty
|
|
vegTasks->MarkDirtySectors(newBounds, threadData->m_dirtySectorContents,
|
|
cachedMainThreadData.m_worldToSector, cachedMainThreadData.m_currViewRect);
|
|
vegTasks->MarkDirtySectors(newBounds, threadData->m_dirtySectorSurfacePoints,
|
|
cachedMainThreadData.m_worldToSector, cachedMainThreadData.m_currViewRect);
|
|
});
|
|
}
|
|
|
|
void AreaSystemComponent::EnumerateInstancesInOverlappingSectors(const AZ::Aabb& bounds, AreaSystemEnumerateCallback callback) const
|
|
{
|
|
AZ_PROFILE_FUNCTION(Entity);
|
|
|
|
if (!bounds.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get the minimum sector that overlaps the bounds, expanded outward based on sectorSearchPadding.
|
|
const SectorId minSector = GetSectorId(bounds.GetMin(), m_worldToSector);
|
|
AZ::Aabb minBounds = m_vegTasks.GetSectorBounds(SectorId(minSector.first - m_configuration.m_sectorSearchPadding,
|
|
minSector.second - m_configuration.m_sectorSearchPadding),
|
|
m_configuration.m_sectorSizeInMeters);
|
|
|
|
// Get the maximum sector that overlaps the bounds, expanded outward based on sectorSearchPadding.
|
|
const SectorId maxSector = GetSectorId(bounds.GetMax(), m_worldToSector);
|
|
AZ::Aabb maxBounds = m_vegTasks.GetSectorBounds(SectorId(maxSector.first + m_configuration.m_sectorSearchPadding,
|
|
maxSector.second + m_configuration.m_sectorSearchPadding),
|
|
m_configuration.m_sectorSizeInMeters);
|
|
|
|
// Use the expanded bounds to enumerate through all instances.
|
|
AZ::Aabb expandedBounds(minBounds);
|
|
expandedBounds.AddAabb(maxBounds);
|
|
EnumerateInstancesInAabb(expandedBounds, callback);
|
|
}
|
|
|
|
void AreaSystemComponent::EnumerateInstancesInAabb(const AZ::Aabb& bounds, AreaSystemEnumerateCallback callback) const
|
|
{
|
|
AZ_PROFILE_FUNCTION(Entity);
|
|
|
|
if (!bounds.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const SectorId minSector = GetSectorId(bounds.GetMin(), m_worldToSector);
|
|
const int minX = minSector.first;
|
|
const int minY = minSector.second;
|
|
const SectorId maxSector = GetSectorId(bounds.GetMax(), m_worldToSector);
|
|
const int maxX = maxSector.first;
|
|
const int maxY = maxSector.second;
|
|
|
|
// Lock the rolling window mutex for the entire enumerate to ensure that our set of sectors doesn't change during the loops.
|
|
AZStd::lock_guard<decltype(m_vegTasks.m_sectorRollingWindowMutex)> lock(m_vegTasks.m_sectorRollingWindowMutex);
|
|
for (int currY = minY; currY <= maxY; ++currY)
|
|
{
|
|
for (int currX = minX; currX <= maxX; ++currX)
|
|
{
|
|
const SectorInfo* sectorInfo = m_vegTasks.GetSector(SectorId(currX, currY));
|
|
if (sectorInfo) // manual sector id's can be outside the active area
|
|
{
|
|
for (const auto& claimPair : sectorInfo->m_claimedWorldPoints)
|
|
{
|
|
const auto& instanceData = claimPair.second;
|
|
if (bounds.Contains(instanceData.m_position))
|
|
{
|
|
if (callback(instanceData) != AreaSystemEnumerateCallbackResult::KeepEnumerating)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
AZStd::size_t AreaSystemComponent::GetInstanceCountInAabb(const AZ::Aabb& bounds) const
|
|
{
|
|
AZStd::size_t result = 0;
|
|
|
|
EnumerateInstancesInAabb(bounds, [&result](const auto&)
|
|
{
|
|
++result;
|
|
return AreaSystemEnumerateCallbackResult::KeepEnumerating;
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
AZStd::vector<Vegetation::InstanceData> AreaSystemComponent::GetInstancesInAabb(const AZ::Aabb& bounds) const
|
|
{
|
|
AZStd::vector<Vegetation::InstanceData> instanceList;
|
|
|
|
EnumerateInstancesInAabb(bounds, [&instanceList](const auto& instance)
|
|
{
|
|
instanceList.push_back(instance);
|
|
return AreaSystemEnumerateCallbackResult::KeepEnumerating;
|
|
});
|
|
|
|
return instanceList;
|
|
}
|
|
|
|
void AreaSystemComponent::GetPointsPerMeter(float& value) const
|
|
{
|
|
if (m_configuration.m_sectorDensity <= 0 || m_configuration.m_sectorSizeInMeters <= 0.0f)
|
|
{
|
|
value = 1.0f;
|
|
}
|
|
else
|
|
{
|
|
value = static_cast<float>(m_configuration.m_sectorDensity) / m_configuration.m_sectorSizeInMeters;
|
|
}
|
|
}
|
|
|
|
void AreaSystemComponent::OnTick(float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time)
|
|
{
|
|
AZ_PROFILE_FUNCTION(Entity);
|
|
|
|
if (m_configuration.m_sectorSizeInMeters < 0)
|
|
{
|
|
m_configuration.m_sectorSizeInMeters = 1;
|
|
}
|
|
m_worldToSector = 1.0f / m_configuration.m_sectorSizeInMeters;
|
|
m_vegetationThreadTaskTimer -= deltaTime;
|
|
|
|
// Check to see if any vegetation data has changed since last tick, and if so, offload the updates to a vegetation thread.
|
|
// - If the thread is currently stopped, check for data changes and start up the thread if changes are detected.
|
|
// - If the thread has an interrupt requested, wait for the interrupt to stop the thread before checking and potentially running again.
|
|
// - If the thread is currently running, only update if the data on the vegetation thread is currently synced with this thread.
|
|
// If the state is currently Updating or Dirty, wait for the vegetation thread to pick up the changes before trying to update the
|
|
// data again to avoid redundant mutex locks or the potential for mismatched state.
|
|
|
|
if ((m_threadData.m_vegetationThreadState == PersistentThreadData::VegetationThreadState::Stopped) ||
|
|
((m_threadData.m_vegetationThreadState == PersistentThreadData::VegetationThreadState::Running) &&
|
|
(m_threadData.m_vegetationDataSyncState == PersistentThreadData::VegetationDataSyncState::Synchronized)))
|
|
{
|
|
bool updateVegetationData = false;
|
|
|
|
// if the config changes, we need to update the vegetation data
|
|
if (ApplyPendingConfigChanges())
|
|
{
|
|
updateVegetationData = true;
|
|
}
|
|
|
|
// if the view rectangle changes, we need to update the vegetation data
|
|
if (CalculateViewRect())
|
|
{
|
|
updateVegetationData = true;
|
|
}
|
|
|
|
if (m_vegetationThreadTaskTimer <= 0.0f)
|
|
{
|
|
m_vegetationThreadTaskTimer = m_configuration.m_threadProcessingIntervalMs * 0.001f;
|
|
|
|
// If there are still vegetation tasks pending and we've waited the requested amount of time between queue checks,
|
|
// then we need to update the vegetation data.
|
|
if (m_vegTasks.VegetationThreadTasksPending())
|
|
{
|
|
updateVegetationData = true;
|
|
}
|
|
}
|
|
|
|
if (updateVegetationData)
|
|
{
|
|
// Our main thread has potentially updated its state, so cache a new copy of the pieces of state we need.
|
|
{
|
|
m_cachedMainThreadData.m_currViewRect = m_currViewRect;
|
|
m_cachedMainThreadData.m_worldToSector = m_worldToSector;
|
|
m_cachedMainThreadData.m_sectorSizeInMeters = m_configuration.m_sectorSizeInMeters;
|
|
m_cachedMainThreadData.m_sectorDensity = m_configuration.m_sectorDensity;
|
|
m_cachedMainThreadData.m_sectorPointSnapMode = m_configuration.m_sectorPointSnapMode;
|
|
}
|
|
|
|
// Set the state to Dirty to signal the thread that it will need to pull a new copy of the main thread state data
|
|
// and refresh the set of work that it's currently doing. The thread will detect the change next time it looks
|
|
// for work and clear the state after it pulls new data.
|
|
m_threadData.m_vegetationDataSyncState = PersistentThreadData::VegetationDataSyncState::Dirty;
|
|
|
|
// If the thread isn't currently running, start it up.
|
|
if (m_threadData.m_vegetationThreadState == PersistentThreadData::VegetationThreadState::Stopped)
|
|
{
|
|
//create a job to process vegetation areas, tasks, sectors in the background
|
|
m_threadData.m_vegetationThreadState = PersistentThreadData::VegetationThreadState::Running;
|
|
auto job = AZ::CreateJobFunction([this]()
|
|
{
|
|
AZ_PROFILE_SCOPE(Entity, "Vegetation::AreaSystemComponent::VegetationThread");
|
|
|
|
UpdateContext context;
|
|
context.Run(&m_threadData, &m_vegTasks, &m_cachedMainThreadData);
|
|
|
|
// After we're done processing as much as we can, clear our thread states and exit.
|
|
m_threadData.m_vegetationThreadState = PersistentThreadData::VegetationThreadState::Stopped;
|
|
m_threadData.m_vegetationDataSyncState = PersistentThreadData::VegetationDataSyncState::Synchronized;
|
|
}, true);
|
|
job->Start();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void AreaSystemComponent::OnTerrainDataCreateBegin()
|
|
{
|
|
// Interrupt any in-process updates until the next tick. We don't want to update
|
|
// while terrain is being created, because we can end up with race conditions in
|
|
// which we're querying terrain for some of the points while terrain is still only
|
|
// partially created. This can happen during creation because the HeightmapModified
|
|
// event fires mid-creation, which can block in TerrainSurfaceDataSystemComponent on
|
|
// the surface data mutex. On the vegetation thread, ModifySurfacePoints in surface
|
|
// components such as RiverSurfaceData can start successfully querying terrain because
|
|
// the TerrainDataRequest bus is now valid, but doesn't always return fully-valid data yet.
|
|
m_threadData.InterruptVegetationThread();
|
|
}
|
|
|
|
void AreaSystemComponent::OnTerrainDataDestroyBegin()
|
|
{
|
|
// Interrupt any in-process updates until the next tick. We don't want to update
|
|
// while terrain is being destroyed. There aren't any *known* race conditions here, but
|
|
// there are likely surface-related race conditions, so it's better to be safe.
|
|
m_threadData.InterruptVegetationThread();
|
|
}
|
|
|
|
bool AreaSystemComponent::CalculateViewRect()
|
|
{
|
|
AZ_PROFILE_FUNCTION(Entity);
|
|
|
|
//Get the active camera.
|
|
bool cameraPositionIsValid = false;
|
|
AZ::Vector3 cameraPosition(0.0f);
|
|
|
|
#ifdef VEGETATION_EDITOR
|
|
Camera::EditorCameraRequestBus::BroadcastResult(cameraPositionIsValid, &Camera::EditorCameraRequestBus::Events::GetActiveCameraPosition, cameraPosition);
|
|
if (!cameraPositionIsValid)
|
|
#endif // VEGETATION_EDITOR
|
|
{
|
|
AZ::EntityId activeCameraId;
|
|
Camera::CameraSystemRequestBus::BroadcastResult(activeCameraId, &Camera::CameraSystemRequests::GetActiveCamera);
|
|
if (activeCameraId.IsValid())
|
|
{
|
|
AZ::TransformBus::EventResult(cameraPosition, activeCameraId, &AZ::TransformInterface::GetWorldTranslation);
|
|
cameraPositionIsValid = true;
|
|
}
|
|
}
|
|
|
|
if (cameraPositionIsValid)
|
|
{
|
|
float posX = cameraPosition.GetX();
|
|
float posY = cameraPosition.GetY();
|
|
|
|
const int sectorSizeInMeters = m_configuration.m_sectorSizeInMeters;
|
|
const int viewSize = m_configuration.m_viewRectangleSize;
|
|
int halfViewSize = viewSize >> 1;
|
|
posX -= halfViewSize * sectorSizeInMeters;
|
|
posY -= halfViewSize * sectorSizeInMeters;
|
|
|
|
auto prevViewRect = m_currViewRect;
|
|
m_currViewRect.m_x = (int)(posX * m_worldToSector);
|
|
m_currViewRect.m_y = (int)(posY * m_worldToSector);
|
|
m_currViewRect.m_width = viewSize;
|
|
m_currViewRect.m_height = viewSize;
|
|
m_currViewRect.m_viewRectBounds =
|
|
AZ::Aabb::CreateFromMinMax(
|
|
AZ::Vector3(
|
|
static_cast<float>(m_currViewRect.m_x * sectorSizeInMeters),
|
|
static_cast<float>(m_currViewRect.m_y * sectorSizeInMeters),
|
|
-AZ::Constants::FloatMax),
|
|
AZ::Vector3(
|
|
static_cast<float>((m_currViewRect.m_x + m_currViewRect.m_width) * sectorSizeInMeters),
|
|
static_cast<float>((m_currViewRect.m_y + m_currViewRect.m_height) * sectorSizeInMeters),
|
|
AZ::Constants::FloatMax));
|
|
|
|
return prevViewRect != m_currViewRect;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
AreaSystemComponent::SectorId AreaSystemComponent::GetSectorId(const AZ::Vector3& worldPos, float worldToSector)
|
|
{
|
|
// Convert world positions into scaled integer sector IDs.
|
|
// The clamp is necessary to ensure that excessive floating-point values don't overflow
|
|
// the sector range. The "nextafter" on the min/max limits are because integer min/max
|
|
// lose precision when converted to float, causing them to grow to a larger range. By
|
|
// using nextafter(), we push them back inside the integer range. Technically, this means
|
|
// there are 128 integer numbers at each end of the range that we aren't using, but in practice
|
|
// there will be many other precision bugs to deal with if we ever start using that range anyways.
|
|
int wx = aznumeric_cast<int>(AZStd::clamp(floor(static_cast<float>(worldPos.GetX() * worldToSector)),
|
|
nextafter(aznumeric_cast<float>(std::numeric_limits<int>::min()), 0.0f),
|
|
nextafter(aznumeric_cast<float>(std::numeric_limits<int>::max()), 0.0f)));
|
|
int wy = aznumeric_cast<int>(AZStd::clamp(floor(static_cast<float>(worldPos.GetY() * worldToSector)),
|
|
nextafter(aznumeric_cast<float>(std::numeric_limits<int>::min()), 0.0f),
|
|
nextafter(aznumeric_cast<float>(std::numeric_limits<int>::max()), 0.0f)));
|
|
return SectorId(wx, wy);
|
|
}
|
|
|
|
AZ_FORCE_INLINE void AreaSystemComponent::ReleaseAllClaims()
|
|
{
|
|
// Interrupt update in process, if any
|
|
m_threadData.InterruptVegetationThread();
|
|
|
|
{
|
|
// Wait for vegetation update thread to finish
|
|
AZStd::lock_guard<decltype(m_threadData.m_vegetationThreadMutex)> lockTasks(m_threadData.m_vegetationThreadMutex);
|
|
|
|
// Synchronously process any queued vegetation thread tasks on the main thread before clearing
|
|
// out the sectors. This allows us to update the active vegetation area lists and mark sectors
|
|
// as dirty prior to clearing them out, so that way we don't refresh them a second time after
|
|
// clearing them out.
|
|
// (only process if the allocation has happened)
|
|
if (!(m_worldToSector <= 0.0f))
|
|
{
|
|
UpdateContext threadContext;
|
|
m_vegTasks.ProcessVegetationThreadTasks(&threadContext, &m_threadData);
|
|
threadContext.UpdateActiveVegetationAreas(&m_threadData, m_currViewRect);
|
|
}
|
|
|
|
// Clear all sector data
|
|
m_vegTasks.ClearSectors();
|
|
}
|
|
}
|
|
|
|
void AreaSystemComponent::ReleaseWithoutCleanup()
|
|
{
|
|
// This method will destroy all active vegetation instances, but leave the vegetation render groups active
|
|
// so that we're ready to process new instances.
|
|
ReleaseAllClaims();
|
|
InstanceSystemRequestBus::Broadcast(&InstanceSystemRequestBus::Events::DestroyAllInstances);
|
|
}
|
|
|
|
void AreaSystemComponent::ReleaseData()
|
|
{
|
|
// This method destroys all active vegetation instances and cleans up / unloads / destroys the vegetation render groups.
|
|
ReleaseAllClaims();
|
|
InstanceSystemRequestBus::Broadcast(&InstanceSystemRequestBus::Events::Cleanup);
|
|
}
|
|
|
|
void AreaSystemComponent::OnCrySystemInitialized(ISystem& system, [[maybe_unused]] const SSystemInitParams& systemInitParams)
|
|
{
|
|
m_system = &system;
|
|
m_system->GetISystemEventDispatcher()->RegisterListener(this);
|
|
}
|
|
|
|
void AreaSystemComponent::OnCrySystemShutdown([[maybe_unused]] ISystem& system)
|
|
{
|
|
if (m_system)
|
|
{
|
|
m_system->GetISystemEventDispatcher()->RemoveListener(this);
|
|
m_system = nullptr;
|
|
}
|
|
}
|
|
|
|
void AreaSystemComponent::OnCryEditorBeginLevelExport()
|
|
{
|
|
// We need to free all our instances before exporting a level to ensure that none of the dynamic vegetation data
|
|
// gets exported into the static vegetation data files.
|
|
|
|
// Clear all our spawned vegetation data so that they don't get written out with the vegetation sectors.
|
|
ReleaseData();
|
|
}
|
|
|
|
void AreaSystemComponent::OnCryEditorEndLevelExport(bool /*success*/)
|
|
{
|
|
// We don't need to do anything here. When the vegetation game components reactivate themselves after the level export completes,
|
|
// (see EditorVegetationComponentBase.h) they will trigger a refresh of the vegetation areas which will produce all our instances again.
|
|
}
|
|
|
|
void AreaSystemComponent::OnCryEditorCloseScene()
|
|
{
|
|
// Clear all our spawned vegetation data
|
|
ReleaseData();
|
|
}
|
|
|
|
|
|
void AreaSystemComponent::OnSystemEvent(ESystemEvent event, [[maybe_unused]] UINT_PTR wparam, [[maybe_unused]] UINT_PTR lparam)
|
|
{
|
|
AZ_PROFILE_FUNCTION(Entity);
|
|
|
|
switch (event)
|
|
{
|
|
case ESYSTEM_EVENT_GAME_MODE_SWITCH_START:
|
|
case ESYSTEM_EVENT_LEVEL_LOAD_START:
|
|
case ESYSTEM_EVENT_LEVEL_UNLOAD:
|
|
case ESYSTEM_EVENT_EDITOR_SIMULATION_MODE_SWITCH_START:
|
|
{
|
|
ReleaseData();
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// VegetationThreadTasks
|
|
|
|
void AreaSystemComponent::VegetationThreadTasks::QueueVegetationTask(AZStd::function<void(UpdateContext * context, PersistentThreadData * threadData, VegetationThreadTasks * vegTasks)> func)
|
|
{
|
|
AZStd::lock_guard<decltype(m_vegetationThreadTaskMutex)> lock(m_vegetationThreadTaskMutex);
|
|
m_vegetationThreadTasks.push_back(func);
|
|
|
|
if (m_debugData)
|
|
{
|
|
m_debugData->m_areaTaskQueueCount.store(static_cast<int>(m_vegetationThreadTasks.size()), AZStd::memory_order_relaxed);
|
|
}
|
|
}
|
|
|
|
void AreaSystemComponent::VegetationThreadTasks::ProcessVegetationThreadTasks(UpdateContext* context, PersistentThreadData* threadData)
|
|
{
|
|
AZ_PROFILE_FUNCTION(Entity);
|
|
|
|
VegetationThreadTasks::VegetationThreadTaskList tasks;
|
|
{
|
|
AZStd::lock_guard<decltype(m_vegetationThreadTaskMutex)> lock(m_vegetationThreadTaskMutex);
|
|
AZStd::swap(tasks, m_vegetationThreadTasks);
|
|
|
|
if (m_debugData)
|
|
{
|
|
m_debugData->m_areaTaskQueueCount.store(static_cast<int>(m_vegetationThreadTasks.size()), AZStd::memory_order_relaxed);
|
|
m_debugData->m_areaTaskActiveCount.store(static_cast<int>(tasks.size()), AZStd::memory_order_relaxed);
|
|
}
|
|
}
|
|
|
|
for (const auto& task : tasks)
|
|
{
|
|
task(context, threadData, this);
|
|
|
|
if (m_debugData)
|
|
{
|
|
m_debugData->m_areaTaskActiveCount.fetch_sub(1, AZStd::memory_order_relaxed);
|
|
}
|
|
}
|
|
}
|
|
|
|
AZ::Aabb AreaSystemComponent::VegetationThreadTasks::GetSectorBounds(const SectorId& sectorId, int sectorSizeInMeters)
|
|
{
|
|
return AZ::Aabb::CreateFromMinMax(
|
|
AZ::Vector3(
|
|
static_cast<float>(sectorId.first * sectorSizeInMeters),
|
|
static_cast<float>(sectorId.second * sectorSizeInMeters),
|
|
-AZ::Constants::FloatMax),
|
|
AZ::Vector3(
|
|
static_cast<float>((sectorId.first + 1) * sectorSizeInMeters),
|
|
static_cast<float>((sectorId.second + 1) * sectorSizeInMeters),
|
|
AZ::Constants::FloatMax));
|
|
}
|
|
|
|
const AreaSystemComponent::SectorInfo* AreaSystemComponent::VegetationThreadTasks::GetSector(const SectorId& sectorId) const
|
|
{
|
|
AZ_PROFILE_FUNCTION(Entity);
|
|
|
|
AZStd::lock_guard<decltype(m_sectorRollingWindowMutex)> lock(m_sectorRollingWindowMutex);
|
|
auto itSector = m_sectorRollingWindow.find(sectorId);
|
|
return itSector != m_sectorRollingWindow.end() ? &itSector->second : nullptr;
|
|
}
|
|
|
|
AreaSystemComponent::SectorInfo* AreaSystemComponent::VegetationThreadTasks::GetSector(const SectorId& sectorId)
|
|
{
|
|
AZ_PROFILE_FUNCTION(Entity);
|
|
|
|
AZStd::lock_guard<decltype(m_sectorRollingWindowMutex)> lock(m_sectorRollingWindowMutex);
|
|
auto itSector = m_sectorRollingWindow.find(sectorId);
|
|
return itSector != m_sectorRollingWindow.end() ? &itSector->second : nullptr;
|
|
}
|
|
|
|
AreaSystemComponent::SectorInfo* AreaSystemComponent::VegetationThreadTasks::CreateSector(const SectorId& sectorId, int sectorDensity, int sectorSizeInMeters, SnapMode sectorPointSnapMode)
|
|
{
|
|
AZ_PROFILE_FUNCTION(Entity);
|
|
|
|
SectorInfo sectorInfo;
|
|
sectorInfo.m_id = sectorId;
|
|
sectorInfo.m_bounds = GetSectorBounds(sectorId, sectorSizeInMeters);
|
|
UpdateSectorPoints(sectorInfo, sectorDensity, sectorSizeInMeters, sectorPointSnapMode);
|
|
|
|
AZStd::lock_guard<decltype(m_sectorRollingWindowMutex)> lock(m_sectorRollingWindowMutex);
|
|
SectorInfo& sectorInfoRef = m_sectorRollingWindow[sectorInfo.m_id] = AZStd::move(sectorInfo);
|
|
UpdateSectorCallbacks(sectorInfoRef);
|
|
return §orInfoRef;
|
|
}
|
|
|
|
void AreaSystemComponent::VegetationThreadTasks::UpdateSectorPoints(SectorInfo& sectorInfo, int sectorDensity, int sectorSizeInMeters, SnapMode sectorPointSnapMode)
|
|
{
|
|
AZ_PROFILE_FUNCTION(Entity);
|
|
const float vegStep = sectorSizeInMeters / static_cast<float>(sectorDensity);
|
|
|
|
//build a free list of all points in the sector for areas to consume
|
|
sectorInfo.m_baseContext.m_masks.clear();
|
|
sectorInfo.m_baseContext.m_availablePoints.clear();
|
|
sectorInfo.m_baseContext.m_availablePoints.reserve(sectorDensity * sectorDensity);
|
|
|
|
// Determine within our texel area where we want to create our vegetation positions:
|
|
// 0 = lower left corner, 0.5 = center
|
|
const float texelOffset = (sectorPointSnapMode == SnapMode::Center) ? 0.5f : 0.0f;
|
|
|
|
SurfaceData::SurfacePointListPerPosition availablePointsPerPosition;
|
|
AZ::Vector2 stepSize(vegStep, vegStep);
|
|
AZ::Vector3 regionOffset(texelOffset * vegStep, texelOffset * vegStep, 0.0f);
|
|
AZ::Aabb regionBounds = sectorInfo.m_bounds;
|
|
regionBounds.SetMin(regionBounds.GetMin() + regionOffset);
|
|
|
|
// If we just used the sector bounds, floating-point error could sometimes cause an extra point to get generated
|
|
// right at the max edge of the bounds. So instead, we adjust our max placement bounds to be the exact size needed
|
|
// for sectorDensity points to get placed, plus half a vegStep to account for a safe margin of floating-point error.
|
|
// The exact size would be (sectorDensity - 1), so adding half a vegStep gives us (sectorDensity - 0.5f).
|
|
// (We should be able to add anything less than 1 extra vegStep and still get exactly sectorDensity points)
|
|
regionBounds.SetMax(regionBounds.GetMin() + AZ::Vector3(vegStep * (sectorDensity - 0.5f),
|
|
vegStep * (sectorDensity - 0.5f), 0.0f));
|
|
|
|
SurfaceData::SurfaceDataSystemRequestBus::Broadcast(
|
|
&SurfaceData::SurfaceDataSystemRequestBus::Events::GetSurfacePointsFromRegion,
|
|
regionBounds,
|
|
stepSize,
|
|
SurfaceData::SurfaceTagVector(),
|
|
availablePointsPerPosition);
|
|
|
|
AZ_Assert(availablePointsPerPosition.size() == (sectorDensity * sectorDensity),
|
|
"Veg sector ended up with unexpected density (%d points created, %d expected)", availablePointsPerPosition.size(),
|
|
(sectorDensity * sectorDensity));
|
|
|
|
uint claimIndex = 0;
|
|
for (auto& availablePoints : availablePointsPerPosition)
|
|
{
|
|
for (auto& surfacePoint : availablePoints.second)
|
|
{
|
|
sectorInfo.m_baseContext.m_availablePoints.push_back();
|
|
ClaimPoint& claimPoint = sectorInfo.m_baseContext.m_availablePoints.back();
|
|
claimPoint.m_handle = CreateClaimHandle(sectorInfo, ++claimIndex);
|
|
claimPoint.m_position = surfacePoint.m_position;
|
|
claimPoint.m_normal = surfacePoint.m_normal;
|
|
claimPoint.m_masks = surfacePoint.m_masks;
|
|
SurfaceData::AddMaxValueForMasks(sectorInfo.m_baseContext.m_masks, surfacePoint.m_masks);
|
|
}
|
|
}
|
|
}
|
|
|
|
void AreaSystemComponent::VegetationThreadTasks::UpdateSectorCallbacks(SectorInfo& sectorInfo)
|
|
{
|
|
//setup callback to test if matching point is already claimed
|
|
sectorInfo.m_baseContext.m_existedCallback = [this, §orInfo](const ClaimPoint& point, const InstanceData& instanceData)
|
|
{
|
|
const ClaimHandle handle = point.m_handle;
|
|
auto claimItr = sectorInfo.m_claimedWorldPointsBeforeFill.find(handle);
|
|
bool exists = (claimItr != sectorInfo.m_claimedWorldPointsBeforeFill.end()) && InstanceData::IsSameInstanceData(instanceData, claimItr->second);
|
|
|
|
if (exists)
|
|
{
|
|
CreateClaim(sectorInfo, handle, instanceData);
|
|
VEG_PROFILE_METHOD(DebugNotificationBus::TryQueueBroadcast(&DebugNotificationBus::Events::CreateInstance, instanceData.m_instanceId, instanceData.m_position, instanceData.m_id));
|
|
}
|
|
|
|
return exists;
|
|
};
|
|
|
|
//setup callback to create claims for new instances
|
|
sectorInfo.m_baseContext.m_createdCallback = [this, §orInfo](const ClaimPoint& point, const InstanceData& instanceData)
|
|
{
|
|
const ClaimHandle handle = point.m_handle;
|
|
auto claimItr = sectorInfo.m_claimedWorldPointsBeforeFill.find(handle);
|
|
bool exists = claimItr != sectorInfo.m_claimedWorldPointsBeforeFill.end();
|
|
|
|
if (exists)
|
|
{
|
|
const auto& claimedInstanceData = claimItr->second;
|
|
if (claimedInstanceData.m_id != instanceData.m_id)
|
|
{
|
|
//must force bus connect if areas are different
|
|
AreaNotificationBus::Event(claimedInstanceData.m_id, &AreaNotificationBus::Events::OnAreaConnect);
|
|
AreaRequestBus::Event(claimedInstanceData.m_id, &AreaRequestBus::Events::UnclaimPosition, handle);
|
|
AreaNotificationBus::Event(claimedInstanceData.m_id, &AreaNotificationBus::Events::OnAreaDisconnect);
|
|
}
|
|
else
|
|
{
|
|
//already connected during fill sector
|
|
AreaRequestBus::Event(claimedInstanceData.m_id, &AreaRequestBus::Events::UnclaimPosition, handle);
|
|
}
|
|
}
|
|
|
|
CreateClaim(sectorInfo, handle, instanceData);
|
|
};
|
|
}
|
|
|
|
void AreaSystemComponent::VegetationThreadTasks::DeleteSector(const SectorId& sectorId)
|
|
{
|
|
AZ_PROFILE_FUNCTION(Entity);
|
|
|
|
AZStd::lock_guard<decltype(m_sectorRollingWindowMutex)> lock(m_sectorRollingWindowMutex);
|
|
auto itSector = m_sectorRollingWindow.find(sectorId);
|
|
if (itSector != m_sectorRollingWindow.end())
|
|
{
|
|
SectorInfo& sectorInfo(itSector->second);
|
|
EmptySector(sectorInfo);
|
|
m_sectorRollingWindow.erase(itSector);
|
|
}
|
|
else
|
|
{
|
|
AZ_Assert(false, "Sector marked for deletion but doesn't exist");
|
|
}
|
|
}
|
|
|
|
template<class Fn>
|
|
inline void AreaSystemComponent::VegetationThreadTasks::EnumerateSectorsInAabb(const AZ::Aabb& bounds, float worldToSector, const ViewRect& viewRect, Fn&& fn)
|
|
{
|
|
// Get the min/max sectors for the AABB. If an invalid AABB is passed in, process every active sector.
|
|
// (i.e. every sector in the current m_currViewRect)
|
|
const SectorId boundsMinSector = bounds.IsValid() ? GetSectorId(bounds.GetMin(), worldToSector) : viewRect.GetMinSector();
|
|
const SectorId boundsMaxSector = bounds.IsValid() ? GetSectorId(bounds.GetMax(), worldToSector) : viewRect.GetMaxSector();
|
|
|
|
// The min bounds are set to the larger of the AABB min and the curr view rect min.
|
|
// The max bounds are set to the smaller of the AABB max and the curr view rect max.
|
|
// This lets us process only the sectors that overlap both.
|
|
// Note that if the AABB doesn't overlap the curr view rect, the max will end up less
|
|
// than the min, in which case we process no sectors.
|
|
const int minX = AZStd::GetMax(boundsMinSector.first, viewRect.GetMinXSector());
|
|
const int minY = AZStd::GetMax(boundsMinSector.second, viewRect.GetMinYSector());
|
|
|
|
const int maxX = AZStd::GetMin(boundsMaxSector.first, viewRect.GetMaxXSector());
|
|
const int maxY = AZStd::GetMin(boundsMaxSector.second, viewRect.GetMaxYSector());
|
|
|
|
for (int currY = minY; currY <= maxY; ++currY)
|
|
{
|
|
for (int currX = minX; currX <= maxX; ++currX)
|
|
{
|
|
if (!fn(AZStd::move(SectorId(currX, currY))))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void AreaSystemComponent::VegetationThreadTasks::AddUnregisteredVegetationArea(const VegetationAreaInfo& area, float worldToSector, const ViewRect& viewRect)
|
|
{
|
|
EnumerateSectorsInAabb(area.m_bounds, worldToSector, viewRect,
|
|
[&](SectorId&& sectorId)
|
|
{
|
|
m_unregisteredVegetationAreaSet[AZStd::move(sectorId)].insert(area.m_id);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
void AreaSystemComponent::VegetationThreadTasks::ReleaseUnregisteredClaims(SectorInfo& sectorInfo)
|
|
{
|
|
AZ_PROFILE_FUNCTION(Entity);
|
|
|
|
if (!m_unregisteredVegetationAreaSet.empty())
|
|
{
|
|
auto unregisteredAreasForSector = m_unregisteredVegetationAreaSet.find(sectorInfo.m_id);
|
|
if (unregisteredAreasForSector != m_unregisteredVegetationAreaSet.end())
|
|
{
|
|
for (auto claimItr = sectorInfo.m_claimedWorldPoints.begin(); claimItr != sectorInfo.m_claimedWorldPoints.end(); )
|
|
{
|
|
if (unregisteredAreasForSector->second.find(claimItr->second.m_id) != unregisteredAreasForSector->second.end())
|
|
{
|
|
claimItr = sectorInfo.m_claimedWorldPoints.erase(claimItr);
|
|
}
|
|
else
|
|
{
|
|
++claimItr;
|
|
}
|
|
}
|
|
|
|
m_unregisteredVegetationAreaSet.erase(unregisteredAreasForSector);
|
|
}
|
|
}
|
|
}
|
|
|
|
void AreaSystemComponent::VegetationThreadTasks::ReleaseUnusedClaims(SectorInfo& sectorInfo)
|
|
{
|
|
AZ_PROFILE_FUNCTION(Entity);
|
|
|
|
AZStd::unordered_map<AZ::EntityId, AZStd::unordered_set<ClaimHandle>> claimsToRelease;
|
|
|
|
// Group up all the previously-claimed-but-no-longer-claimed points based on area id
|
|
for (const auto& claimPair : sectorInfo.m_claimedWorldPointsBeforeFill)
|
|
{
|
|
const auto& handle = claimPair.first;
|
|
const auto& instanceData = claimPair.second;
|
|
const auto& areaId = instanceData.m_id;
|
|
if (sectorInfo.m_claimedWorldPoints.find(handle) == sectorInfo.m_claimedWorldPoints.end())
|
|
{
|
|
claimsToRelease[areaId].insert(handle);
|
|
}
|
|
}
|
|
sectorInfo.m_claimedWorldPointsBeforeFill.clear();
|
|
|
|
// Iterate over the claims by area id and release them
|
|
for (const auto& claimPair : claimsToRelease)
|
|
{
|
|
const auto& areaId = claimPair.first;
|
|
const auto& handles = claimPair.second;
|
|
AreaNotificationBus::Event(areaId, &AreaNotificationBus::Events::OnAreaConnect);
|
|
|
|
for (const auto& handle : handles)
|
|
{
|
|
AreaRequestBus::Event(areaId, &AreaRequestBus::Events::UnclaimPosition, handle);
|
|
}
|
|
|
|
AreaNotificationBus::Event(areaId, &AreaNotificationBus::Events::OnAreaDisconnect);
|
|
}
|
|
}
|
|
|
|
void AreaSystemComponent::VegetationThreadTasks::FillSector(SectorInfo& sectorInfo, const VegetationAreaVector& activeAreas)
|
|
{
|
|
AZ_PROFILE_FUNCTION(Entity);
|
|
VEG_PROFILE_METHOD(DebugNotificationBus::TryQueueBroadcast(&DebugNotificationBus::Events::FillSectorStart, sectorInfo.GetSectorX(), sectorInfo.GetSectorY(), AZStd::chrono::system_clock::now()));
|
|
|
|
ReleaseUnregisteredClaims(sectorInfo);
|
|
|
|
//m_availablePoints is a free list initialized with the complete set of points in the sector.
|
|
ClaimContext activeContext = sectorInfo.m_baseContext;
|
|
|
|
// Clear out the list of claimed world points before we begin
|
|
sectorInfo.m_claimedWorldPointsBeforeFill = sectorInfo.m_claimedWorldPoints;
|
|
sectorInfo.m_claimedWorldPoints.clear();
|
|
|
|
//for all active areas attempt to spawn vegetation on sector grid positions
|
|
for (const auto& area : activeAreas)
|
|
{
|
|
//if one or more areas claimed all the points in m_availablePoints, there's no reason to continue.
|
|
if (activeContext.m_availablePoints.empty())
|
|
{
|
|
break;
|
|
}
|
|
|
|
//only consider areas that intersect this sector
|
|
if (!area.m_bounds.IsValid() || area.m_bounds.Overlaps(sectorInfo.m_bounds))
|
|
{
|
|
VEG_PROFILE_METHOD(DebugNotificationBus::TryQueueBroadcast(&DebugNotificationBus::Events::FillAreaStart, area.m_id, AZStd::chrono::system_clock::now()));
|
|
|
|
//each area is responsible for removing whatever points it claims from m_availablePoints, so subsequent areas will have fewer points to try to claim.
|
|
AreaNotificationBus::Event(area.m_id, &AreaNotificationBus::Events::OnAreaConnect);
|
|
AreaRequestBus::Event(area.m_id, &AreaRequestBus::Events::ClaimPositions, EntityIdStack{}, activeContext);
|
|
AreaNotificationBus::Event(area.m_id, &AreaNotificationBus::Events::OnAreaDisconnect);
|
|
|
|
VEG_PROFILE_METHOD(DebugNotificationBus::TryQueueBroadcast(&DebugNotificationBus::Events::FillAreaEnd, area.m_id, AZStd::chrono::system_clock::now(), aznumeric_cast<AZ::u32>(activeContext.m_availablePoints.size())));
|
|
}
|
|
}
|
|
|
|
ReleaseUnusedClaims(sectorInfo);
|
|
|
|
VEG_PROFILE_METHOD(DebugNotificationBus::TryQueueBroadcast(&DebugNotificationBus::Events::FillSectorEnd, sectorInfo.GetSectorX(), sectorInfo.GetSectorY(), AZStd::chrono::system_clock::now(), aznumeric_cast<AZ::u32>(activeContext.m_availablePoints.size())));
|
|
}
|
|
|
|
void AreaSystemComponent::VegetationThreadTasks::EmptySector(SectorInfo& sectorInfo)
|
|
{
|
|
AZ_PROFILE_FUNCTION(Entity);
|
|
|
|
AZStd::unordered_map<AZ::EntityId, AZStd::unordered_set<ClaimHandle>> claimsToRelease;
|
|
|
|
// group up all the points based on area id
|
|
for (const auto& claimPair : sectorInfo.m_claimedWorldPoints)
|
|
{
|
|
const auto& handle = claimPair.first;
|
|
const auto& instanceData = claimPair.second;
|
|
const auto& areaId = instanceData.m_id;
|
|
claimsToRelease[areaId].insert(handle);
|
|
}
|
|
sectorInfo.m_claimedWorldPoints.clear();
|
|
|
|
// iterate over the claims by area id and release them
|
|
for (const auto& claimPair : claimsToRelease)
|
|
{
|
|
const auto& areaId = claimPair.first;
|
|
const auto& handles = claimPair.second;
|
|
AreaNotificationBus::Event(areaId, &AreaNotificationBus::Events::OnAreaConnect);
|
|
|
|
for (const auto& handle : handles)
|
|
{
|
|
AreaRequestBus::Event(areaId, &AreaRequestBus::Events::UnclaimPosition, handle);
|
|
}
|
|
|
|
AreaNotificationBus::Event(areaId, &AreaNotificationBus::Events::OnAreaDisconnect);
|
|
}
|
|
}
|
|
|
|
void AreaSystemComponent::VegetationThreadTasks::ClearSectors()
|
|
{
|
|
AZ_PROFILE_FUNCTION(Entity);
|
|
|
|
AZStd::lock_guard<decltype(m_sectorRollingWindowMutex)> lock(m_sectorRollingWindowMutex);
|
|
for (auto& sectorPair : m_sectorRollingWindow)
|
|
{
|
|
EmptySector(sectorPair.second);
|
|
}
|
|
m_sectorRollingWindow.clear();
|
|
|
|
// Clear any pending unregistrations; since all of the sectors have been cleared anyways, these don't affect anything
|
|
m_unregisteredVegetationAreaSet.clear();
|
|
}
|
|
|
|
void AreaSystemComponent::VegetationThreadTasks::CreateClaim(SectorInfo& sectorInfo, const ClaimHandle handle, const InstanceData& instanceData)
|
|
{
|
|
AZ_PROFILE_FUNCTION(Entity);
|
|
sectorInfo.m_claimedWorldPoints[handle] = instanceData;
|
|
}
|
|
|
|
ClaimHandle AreaSystemComponent::VegetationThreadTasks::CreateClaimHandle(const SectorInfo& sectorInfo, uint32_t index) const
|
|
{
|
|
AZ_PROFILE_FUNCTION(Entity);
|
|
|
|
ClaimHandle handle = 0;
|
|
AreaSystemUtil::hash_combine_64(handle, sectorInfo.m_id.first);
|
|
AreaSystemUtil::hash_combine_64(handle, sectorInfo.m_id.second);
|
|
AreaSystemUtil::hash_combine_64(handle, index);
|
|
return handle;
|
|
}
|
|
|
|
void AreaSystemComponent::VegetationThreadTasks::MarkDirtySectors(const AZ::Aabb& bounds, DirtySectors& dirtySet, float worldToSector, const ViewRect& viewRect)
|
|
{
|
|
if (bounds.IsValid())
|
|
{
|
|
if (!dirtySet.IsAllDirty())
|
|
{
|
|
// Only mark individual sectors as dirty if we have valid AABB bounds and haven't
|
|
// already marked *all* sectors as dirty.
|
|
EnumerateSectorsInAabb(bounds, worldToSector, viewRect, [&](SectorId&& sectorId)
|
|
{
|
|
dirtySet.MarkDirty(sectorId);
|
|
return true;
|
|
});
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If we have invalid bounds, we can mark all sectors as dirty without needing
|
|
// to add each one to the list.
|
|
dirtySet.MarkAllDirty();
|
|
}
|
|
}
|
|
|
|
void AreaSystemComponent::VegetationThreadTasks::FetchDebugData()
|
|
{
|
|
VEG_PROFILE_METHOD(DebugSystemDataBus::BroadcastResult(m_debugData, &DebugSystemDataBus::Events::GetDebugData));
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// PersistentThreadData
|
|
|
|
AZ_FORCE_INLINE void AreaSystemComponent::PersistentThreadData::InterruptVegetationThread()
|
|
{
|
|
auto expected = PersistentThreadData::VegetationThreadState::Running;
|
|
m_vegetationThreadState.compare_exchange_strong(expected, PersistentThreadData::VegetationThreadState::InterruptRequested);
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// UpdateContext
|
|
|
|
void AreaSystemComponent::UpdateContext::Run(PersistentThreadData* threadData, VegetationThreadTasks* vegTasks, CachedMainThreadData* cachedMainThreadData)
|
|
{
|
|
AZ_PROFILE_FUNCTION(Entity);
|
|
|
|
// Ensure that the main thread doesn't activate or deactivate the component until after this thread finishes.
|
|
// Note that this does *not* prevent the main thread from running OnTick, which can communicate data changes
|
|
// to this thread while it's still processing work.
|
|
AZStd::lock_guard<decltype(threadData->m_vegetationThreadMutex)> lockTasks(threadData->m_vegetationThreadMutex);
|
|
|
|
bool keepProcessing = true;
|
|
while (keepProcessing && (threadData->m_vegetationThreadState != PersistentThreadData::VegetationThreadState::InterruptRequested))
|
|
{
|
|
AZ_PROFILE_SCOPE(Entity, "Vegetation::AreaSystemComponent::UpdateContext::Run-InnerLoop");
|
|
// Update thread state if its dirty
|
|
PersistentThreadData::VegetationDataSyncState expected = PersistentThreadData::VegetationDataSyncState::Dirty;
|
|
if (threadData->m_vegetationDataSyncState.compare_exchange_strong(expected, PersistentThreadData::VegetationDataSyncState::Updating))
|
|
{
|
|
// A dirty state can consist of one or more of the following:
|
|
// - Main thread has changed veg configuration
|
|
// - Main thread has changed current view rectangle
|
|
// - Vegetation tasks have been queued for this thread to process
|
|
|
|
// Our main thread has potentially updated its state, so cache a new copy of the pieces of state we need.
|
|
m_cachedMainThreadData = *cachedMainThreadData;
|
|
|
|
// Run through all the queued tasks to update vegetation area active states and lists of dirty sectors
|
|
vegTasks->ProcessVegetationThreadTasks(this, threadData);
|
|
|
|
// Now that we've processed all the queued tasks, gather a list of active areas that affect our visible sectors, sorted by priority
|
|
UpdateActiveVegetationAreas(threadData, m_cachedMainThreadData.m_currViewRect);
|
|
|
|
// Refresh the lists of sectors to create / update / remove
|
|
keepProcessing = UpdateSectorWorkLists(threadData, vegTasks);
|
|
|
|
// We've finished refreshing the thread work state, so mark ourselves as synchronized.
|
|
threadData->m_vegetationDataSyncState = PersistentThreadData::VegetationDataSyncState::Synchronized;
|
|
}
|
|
|
|
if (keepProcessing)
|
|
{
|
|
keepProcessing = UpdateOneSector(threadData, vegTasks);
|
|
}
|
|
}
|
|
}
|
|
|
|
void AreaSystemComponent::UpdateContext::UpdateActiveVegetationAreas(PersistentThreadData* threadData, const ViewRect& viewRect)
|
|
{
|
|
AZ_PROFILE_FUNCTION(Entity);
|
|
|
|
//build a priority sorted list of all active areas
|
|
if (threadData->m_activeAreasDirty)
|
|
{
|
|
threadData->m_activeAreasDirty = false;
|
|
threadData->m_activeAreas.clear();
|
|
threadData->m_activeAreas.reserve(threadData->m_globalVegetationAreaMap.size());
|
|
for (const auto& areaPair : threadData->m_globalVegetationAreaMap)
|
|
{
|
|
const auto& area = areaPair.second;
|
|
|
|
//if this is an area being ignored due to a parent area blender, skip it
|
|
if (threadData->m_ignoredVegetationAreaSet.find(area.m_id) != threadData->m_ignoredVegetationAreaSet.end())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
//do any per area setup or checks since the state of areas and entities with the system has changed
|
|
bool prepared = false;
|
|
AreaNotificationBus::Event(area.m_id, &AreaNotificationBus::Events::OnAreaConnect);
|
|
AreaRequestBus::EventResult(prepared, area.m_id, &AreaRequestBus::Events::PrepareToClaim, EntityIdStack{});
|
|
AreaNotificationBus::Event(area.m_id, &AreaNotificationBus::Events::OnAreaDisconnect);
|
|
if (!prepared)
|
|
{
|
|
// if PrepareToClaim returned false, this area is declaring itself as inactive.
|
|
// The area will need to call RefreshArea() if/when its state should change to active.
|
|
continue;
|
|
}
|
|
|
|
threadData->m_activeAreas.push_back(area);
|
|
}
|
|
|
|
AZStd::sort(threadData->m_activeAreas.begin(), threadData->m_activeAreas.end(), [](const auto& lhs, const auto& rhs)
|
|
{
|
|
return AZStd::make_pair(lhs.m_layer, lhs.m_priority) > AZStd::make_pair(rhs.m_layer, rhs.m_priority);
|
|
});
|
|
}
|
|
|
|
//further reduce set of active areas to only include ones that intersect the bubble
|
|
AZ::Aabb bubbleBounds = viewRect.GetViewRectBounds();
|
|
threadData->m_activeAreasInBubble = threadData->m_activeAreas;
|
|
threadData->m_activeAreasInBubble.erase(
|
|
AZStd::remove_if(
|
|
threadData->m_activeAreasInBubble.begin(),
|
|
threadData->m_activeAreasInBubble.end(),
|
|
[bubbleBounds](const auto& area) { return area.m_bounds.IsValid() && !area.m_bounds.Overlaps(bubbleBounds); }),
|
|
threadData->m_activeAreasInBubble.end());
|
|
}
|
|
|
|
bool AreaSystemComponent::UpdateContext::UpdateSectorWorkLists(PersistentThreadData* threadData, VegetationThreadTasks* vegTasks)
|
|
{
|
|
AZ_PROFILE_FUNCTION(Entity);
|
|
|
|
auto& worldToSector = m_cachedMainThreadData.m_worldToSector;
|
|
auto& currViewRect = m_cachedMainThreadData.m_currViewRect;
|
|
|
|
// only process the sectors if the allocation has happened
|
|
if (worldToSector <= 0.0f)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool deleteAllSectors = false;
|
|
|
|
// Early exit if no active areas, no sectors are marked as dirty or updating, and there
|
|
// are no sectors left in our rolling window.
|
|
// Until an area becomes active again, there's no work that sectors should need to do.
|
|
if (threadData->m_activeAreasInBubble.empty() &&
|
|
threadData->m_dirtySectorContents.IsNoneDirty() &&
|
|
threadData->m_dirtySectorSurfacePoints.IsNoneDirty())
|
|
{
|
|
AZStd::lock_guard<decltype(vegTasks->m_sectorRollingWindowMutex)> lock(vegTasks->m_sectorRollingWindowMutex);
|
|
if (vegTasks->m_sectorRollingWindow.empty())
|
|
{
|
|
return !m_deleteWorkList.empty() || !m_updateWorkList.empty();
|
|
}
|
|
else
|
|
{
|
|
// No active areas left in our view bubble, so queue up the deletion of all remaining active sectors.
|
|
deleteAllSectors = true;
|
|
}
|
|
}
|
|
|
|
// Cache off the total number of sectors that *should* be active in the view rectangle. We'll use this
|
|
// when processing sectors to ensure that we start to prioritize deletes whenever our number of active sectors
|
|
// gets above this number.
|
|
m_viewRectSectorCount = currViewRect.GetNumSectors();
|
|
|
|
// remove any sectors marked for update which are no longer in the view rectangle
|
|
m_updateWorkList.erase(
|
|
AZStd::remove_if(
|
|
m_updateWorkList.begin(),
|
|
m_updateWorkList.end(),
|
|
[currViewRect](const auto& entry) {return !currViewRect.IsInside(entry.first); }),
|
|
m_updateWorkList.end());
|
|
AZ_Assert(m_updateWorkList.size() <= m_viewRectSectorCount, "Refreshed RequestedUpdate list should not be larger than the view rectangle.");
|
|
|
|
// Clear our delete work list, we'll recreate it and sort it again below.
|
|
// Note: We do NOT clear m_updateWorkList, because we use it to incrementally determine any new
|
|
// updates to add to the queue. Without it, we wouldn't know if a previous data change caused
|
|
// us to mark any sectors still in view as needing an update.
|
|
m_deleteWorkList.clear();
|
|
|
|
// If we're deleting all sectors, make sure we don't have any of them previously queued up for creation / updating.
|
|
if (deleteAllSectors)
|
|
{
|
|
m_updateWorkList.clear();
|
|
}
|
|
|
|
// Run through our list of active sectors and determine which ones need adding / updating / deleting
|
|
{
|
|
AZStd::lock_guard<decltype(vegTasks->m_sectorRollingWindowMutex)> lock(vegTasks->m_sectorRollingWindowMutex);
|
|
|
|
// To create our add / update / delete lists, we need two loops. The first loops through the *new* view rectangle
|
|
// looking for missing sectors to add. The second loops through the *current* set of active sectors looking for any
|
|
// to update or remove.
|
|
|
|
// First loop: Determine non-existent sectors which need to be created
|
|
if (!deleteAllSectors)
|
|
{
|
|
for (int y = currViewRect.m_y; y < currViewRect.m_y + currViewRect.m_height; ++y)
|
|
{
|
|
for (int x = currViewRect.m_x; x < currViewRect.m_x + currViewRect.m_width; ++x)
|
|
{
|
|
SectorId sectorId(x, y);
|
|
if (vegTasks->m_sectorRollingWindow.find(sectorId) == vegTasks->m_sectorRollingWindow.end())
|
|
{
|
|
// If the sector doesn't currently exist and it belongs in the view rect, request a creation.
|
|
// (This will either create a new entry or overwrite an existing pending Create request)
|
|
auto found = AZStd::find_if(m_updateWorkList.begin(), m_updateWorkList.end(), [sectorId](auto& entry) { return (entry.first == sectorId); });
|
|
if (found != m_updateWorkList.end())
|
|
{
|
|
// If the update entry already exists, overwrite the state. We don't need to check or
|
|
// preserve the existing state because Create is the most comprehensive update we can do.
|
|
found->second = UpdateMode::Create;
|
|
}
|
|
else
|
|
{
|
|
m_updateWorkList.emplace_back(sectorId, UpdateMode::Create);
|
|
}
|
|
|
|
// Since we've already removed entries that aren't in the view rect, and these loops are only
|
|
// adding entries in the view rect, at this point our update work list size should never get
|
|
// larger than the set of sectors in the view rect.
|
|
AZ_Assert(m_updateWorkList.size() <= m_viewRectSectorCount, "Too many update requests added");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Second loop: Determine any existing sectors which need to be updated or deleted
|
|
for (auto& sector : vegTasks->m_sectorRollingWindow)
|
|
{
|
|
auto& sectorId = sector.first;
|
|
|
|
if (deleteAllSectors || !currViewRect.IsInside(sectorId))
|
|
{
|
|
// Active sector is no longer within view or there are no active areas, so delete it
|
|
m_deleteWorkList.emplace_back(AZStd::move(sectorId));
|
|
}
|
|
else if (threadData->m_dirtySectorSurfacePoints.IsDirty(sectorId))
|
|
{
|
|
// Active sector has new surface point information, so rebuild surface cache and fill
|
|
// (This will either create a new entry, or overwrite an existing fill or rebuild request)
|
|
auto found = AZStd::find_if(m_updateWorkList.begin(), m_updateWorkList.end(), [sectorId](auto& entry) { return (entry.first == sectorId); });
|
|
if (found != m_updateWorkList.end())
|
|
{
|
|
// If the update entry already exists, overwrite the state. We don't need to check or
|
|
// preserve the state since it should only contain either Rebuild or Fill, and Rebuild
|
|
// is more comprehensive than Fill.
|
|
AZ_Assert(found->second != UpdateMode::Create, "Create requests shouldn't exist for active sectors!");
|
|
found->second = UpdateMode::RebuildSurfaceCacheAndFill;
|
|
}
|
|
else
|
|
{
|
|
m_updateWorkList.emplace_back(sectorId, UpdateMode::RebuildSurfaceCacheAndFill);
|
|
}
|
|
|
|
// We shouldn't ever have an update list that's larger than the set of sectors in the view rect.
|
|
AZ_Assert(m_updateWorkList.size() <= m_viewRectSectorCount, "Too many update requests added");
|
|
}
|
|
else if (threadData->m_dirtySectorContents.IsDirty(sectorId))
|
|
{
|
|
// Active sector has new veg area information, so refill it.
|
|
auto found = AZStd::find_if(m_updateWorkList.begin(), m_updateWorkList.end(), [sectorId](auto& entry)
|
|
{ return (entry.first == sectorId); });
|
|
if (found == m_updateWorkList.end())
|
|
{
|
|
// Only add Fill entries if no update request exists for this sector. We don't
|
|
// overwrite existing entries because an existing entry might have previously
|
|
// requested "RebuildSurfaceCacheAndFill", which is more comprehensive than this request.
|
|
m_updateWorkList.emplace_back(sectorId, UpdateMode::Fill);
|
|
|
|
// We shouldn't ever have an update list that's larger than the set of sectors in the view rect.
|
|
AZ_Assert(m_updateWorkList.size() <= m_viewRectSectorCount, "Too many update requests added");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// We've finished processing our dirtySector lists, so clear them.
|
|
threadData->m_dirtySectorContents.Clear();
|
|
threadData->m_dirtySectorSurfacePoints.Clear();
|
|
|
|
// sort work by distance from center of the view rectangle.
|
|
if (currViewRect.GetViewRectBounds().IsValid())
|
|
{
|
|
float sectorCenterX = (currViewRect.GetMinXSector() + currViewRect.GetMaxXSector()) / 2.0f;
|
|
float sectorCenterY = (currViewRect.GetMinYSector() + currViewRect.GetMaxYSector()) / 2.0f;
|
|
|
|
// Sort function that returns true if the lhs is "closer" than the rhs to the center.
|
|
// The choice of sort algorithm is somewhat a question of preference, and could potentially be made a policy
|
|
// choice at some point. The current choice uses "number of sectors from center" as the primary sort criteria,
|
|
// with a secondary sort on y and x values to get a deterministic sort pattern. This algorithm updates the vegetation
|
|
// outward in cocentric circles.
|
|
// Here are some other possibilities of algorithm choices:
|
|
// 1) float maxDist = AZStd::GetMax(fabs(id.first - sectorCenterX), fabs(id.second - sectorCenterY));
|
|
// This moves outward in cocentric squares.
|
|
// 2) float maxDist = GetSectorBounds(id).GetCenter().GetDistanceSq(currViewRect.GetViewRectBounds().GetCenter());
|
|
// This moves outward in cocentric circles, similar to our chosen algorithm, but in more of a "pinwheel" pattern
|
|
// that fans out from the axis lines.
|
|
// 3) We could feed in camera orientation as well, and use that to further prioritize sectors within view. The concern
|
|
// with choosing this approach is that it will update the work lists much more rapidly than the vegetation can spawn, so
|
|
// it the extra updates and calculations could easily cause sector choices that constantly lag behind the current view,
|
|
// producing similar to worse results than our current algorithm.
|
|
|
|
// With any of these choices, the secondary sort gives a deterministic update pattern when the distances are equal.
|
|
|
|
auto sectorCompare = [sectorCenterX, sectorCenterY](const SectorId& lhsSectorId, const SectorId& rhsSectorId, bool sortClosestFirst)
|
|
{
|
|
const float lhsMaxDist = ((lhsSectorId.first - sectorCenterX) * (lhsSectorId.first - sectorCenterX)) + ((lhsSectorId.second - sectorCenterY) * (lhsSectorId.second - sectorCenterY));
|
|
const float rhsMaxDist = ((rhsSectorId.first - sectorCenterX) * (rhsSectorId.first - sectorCenterX)) + ((rhsSectorId.second - sectorCenterY) * (rhsSectorId.second - sectorCenterY));
|
|
return (lhsMaxDist < rhsMaxDist) ? sortClosestFirst : // Return if one sector is closer than the other to the center.
|
|
((lhsMaxDist > rhsMaxDist) ? !sortClosestFirst :
|
|
((lhsSectorId.second < rhsSectorId.second) ? true : // If it's the same distance return if the Y value is smaller...
|
|
((lhsSectorId.second > rhsSectorId.second) ? false :
|
|
(lhsSectorId.first < rhsSectorId.first)))); // If the Y value is the same, return if the X value is smaller.
|
|
};
|
|
|
|
AZStd::sort(m_updateWorkList.begin(), m_updateWorkList.end(), [sectorCompare](const auto& lhs, const auto& rhs)
|
|
{
|
|
// We always pull from the end of the list, so we sort the *closest* sectors to the end.
|
|
// That way we create / update the closest sectors first.
|
|
return sectorCompare(lhs.first, rhs.first, false);
|
|
});
|
|
|
|
AZStd::sort(m_deleteWorkList.begin(), m_deleteWorkList.end(), [sectorCompare](const auto& lhs, const auto& rhs)
|
|
{
|
|
// We always pull from the end of the list, so we sort the *furthest* sectors to the end.
|
|
// That way we delete the furthest sectors first.
|
|
return sectorCompare(lhs, rhs, true);
|
|
});
|
|
}
|
|
|
|
return !m_deleteWorkList.empty() || !m_updateWorkList.empty();
|
|
}
|
|
|
|
bool AreaSystemComponent::UpdateContext::UpdateOneSector(PersistentThreadData* threadData, VegetationThreadTasks* vegTasks)
|
|
{
|
|
AZ_PROFILE_FUNCTION(Entity);
|
|
|
|
// This chooses work in the following order:
|
|
// 1) Delete if we have more sectors than the total that should be in the view rectangle
|
|
// 2) Create/update if we have any sectors to create / update
|
|
// 3) Delete if we have any sectors to delete
|
|
|
|
// Delete if there are more active sectors than the number of desired sectors or the update list is empty.
|
|
if (!m_deleteWorkList.empty())
|
|
{
|
|
AZStd::lock_guard<decltype(vegTasks->m_sectorRollingWindowMutex)> lock(vegTasks->m_sectorRollingWindowMutex);
|
|
|
|
if ((vegTasks->m_sectorRollingWindow.size() > m_viewRectSectorCount) || m_updateWorkList.empty())
|
|
{
|
|
vegTasks->DeleteSector(m_deleteWorkList.back());
|
|
m_deleteWorkList.pop_back();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Create / update if there's anything to do and we didn't prioritize a delete.
|
|
if (!m_updateWorkList.empty())
|
|
{
|
|
auto& updateEntry = m_updateWorkList.back();
|
|
SectorId sectorId = updateEntry.first;
|
|
UpdateMode mode = updateEntry.second;
|
|
m_updateWorkList.pop_back();
|
|
|
|
{
|
|
AZStd::lock_guard<decltype(vegTasks->m_sectorRollingWindowMutex)> lock(vegTasks->m_sectorRollingWindowMutex);
|
|
|
|
auto& sectorDensity = m_cachedMainThreadData.m_sectorDensity;
|
|
auto& sectorSizeInMeters = m_cachedMainThreadData.m_sectorSizeInMeters;
|
|
auto& sectorPointSnapMode = m_cachedMainThreadData.m_sectorPointSnapMode;
|
|
|
|
switch (mode)
|
|
{
|
|
case UpdateMode::RebuildSurfaceCacheAndFill:
|
|
{
|
|
auto sectorInfo = vegTasks->GetSector(sectorId);
|
|
AZ_Assert(sectorInfo, "Sector update mode is 'RebuildSurfaceCache' but sector doesn't exist");
|
|
vegTasks->UpdateSectorPoints(*sectorInfo, sectorDensity, sectorSizeInMeters, sectorPointSnapMode);
|
|
vegTasks->FillSector(*sectorInfo, threadData->m_activeAreasInBubble);
|
|
}
|
|
break;
|
|
|
|
case UpdateMode::Fill:
|
|
{
|
|
auto sectorInfo = vegTasks->GetSector(sectorId);
|
|
AZ_Assert(sectorInfo, "Sector update mode is 'Fill' but sector doesn't exist");
|
|
vegTasks->FillSector(*sectorInfo, threadData->m_activeAreasInBubble);
|
|
}
|
|
break;
|
|
|
|
case UpdateMode::Create:
|
|
{
|
|
AZ_Assert(!vegTasks->GetSector(sectorId), "Sector update mode is 'Create' but sector already exists");
|
|
auto sectorInfo = vegTasks->CreateSector(sectorId, sectorDensity, sectorSizeInMeters, sectorPointSnapMode);
|
|
vegTasks->FillSector(*sectorInfo, threadData->m_activeAreasInBubble);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// No sectors left to process, so tell our main loop to stop processing.
|
|
return false;
|
|
}
|
|
|
|
}
|