You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
492 lines
23 KiB
C++
492 lines
23 KiB
C++
/*
|
|
* Copyright (c) Contributors to the Open 3D Engine Project
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
*
|
|
*/
|
|
|
|
#pragma once
|
|
|
|
#include <AzCore/Component/Component.h>
|
|
#include <Vegetation/Ebuses/AreaSystemRequestBus.h>
|
|
#include <Vegetation/Ebuses/AreaRequestBus.h>
|
|
#include <Vegetation/Ebuses/SystemConfigurationBus.h>
|
|
#include <AzCore/std/parallel/semaphore.h>
|
|
#include <AzCore/Component/TickBus.h>
|
|
#include <AzCore/std/parallel/thread.h>
|
|
#include <GradientSignal/Ebuses/SectorDataRequestBus.h>
|
|
#include <SurfaceData/SurfaceDataSystemNotificationBus.h>
|
|
#include <CrySystemBus.h>
|
|
#include <StatObjBus.h>
|
|
#include <ISystem.h>
|
|
#include <AzFramework/Terrain/TerrainDataRequestBus.h>
|
|
|
|
namespace Vegetation
|
|
{
|
|
struct DebugData;
|
|
|
|
enum class SnapMode : AZ::u8
|
|
{
|
|
Corner = 0,
|
|
Center
|
|
};
|
|
|
|
/**
|
|
* The configuration for managing areas mostly the dimensions of the sectors
|
|
*/
|
|
class AreaSystemConfig
|
|
: public AZ::ComponentConfig
|
|
{
|
|
public:
|
|
AZ_CLASS_ALLOCATOR(AreaSystemConfig, AZ::SystemAllocator, 0);
|
|
AZ_RTTI(AreaSystemConfig, "{14CCBE43-52DD-4F56-92A8-2BB011A0F7A2}", AZ::ComponentConfig);
|
|
static void Reflect(AZ::ReflectContext* context);
|
|
|
|
bool operator == (const AreaSystemConfig& other) const
|
|
{
|
|
return m_viewRectangleSize == other.m_viewRectangleSize
|
|
&& m_sectorDensity == other.m_sectorDensity
|
|
&& m_sectorSizeInMeters == other.m_sectorSizeInMeters
|
|
&& m_threadProcessingIntervalMs == other.m_threadProcessingIntervalMs
|
|
&& m_sectorSearchPadding == other.m_sectorSearchPadding
|
|
&& m_sectorPointSnapMode == other.m_sectorPointSnapMode;
|
|
}
|
|
|
|
int m_viewRectangleSize = 13;
|
|
int m_sectorDensity = 20;
|
|
int m_sectorSizeInMeters = 16;
|
|
int m_threadProcessingIntervalMs = 500;
|
|
int m_sectorSearchPadding = 0;
|
|
SnapMode m_sectorPointSnapMode = SnapMode::Corner;
|
|
private:
|
|
static const int s_maxViewRectangleSize;
|
|
static const int s_maxSectorDensity;
|
|
static const int s_maxSectorSizeInMeters;
|
|
|
|
static const int s_maxInstancesPerMeter;
|
|
static const int64_t s_maxVegetationInstances;
|
|
|
|
AZ::Outcome<void, AZStd::string> ValidateViewArea(void* newValue, const AZ::Uuid& valueType);
|
|
AZ::Outcome<void, AZStd::string> ValidateSectorDensity(void* newValue, const AZ::Uuid& valueType);
|
|
AZ::Outcome<void, AZStd::string> ValidateSectorSize(void* newValue, const AZ::Uuid& valueType);
|
|
};
|
|
|
|
/**
|
|
* Manages an sectors and claims while the camera scrolls through the 3D world
|
|
*/
|
|
class AreaSystemComponent
|
|
: public AZ::Component
|
|
, private AZ::TickBus::Handler
|
|
, private AreaSystemRequestBus::Handler
|
|
, private GradientSignal::SectorDataRequestBus::Handler
|
|
, private SystemConfigurationRequestBus::Handler
|
|
, private InstanceStatObjEventBus::Handler
|
|
, private CrySystemEventBus::Handler
|
|
, private ISystemEventListener
|
|
, private SurfaceData::SurfaceDataSystemNotificationBus::Handler
|
|
, private AzFramework::Terrain::TerrainDataNotificationBus::Handler
|
|
{
|
|
public:
|
|
friend class EditorAreaSystemComponent;
|
|
AZ_COMPONENT(AreaSystemComponent, "{7CE8E791-6BC6-4C88-8727-A476DE00F9A1}");
|
|
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services);
|
|
static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& services);
|
|
static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& services);
|
|
static void Reflect(AZ::ReflectContext* context);
|
|
|
|
AreaSystemComponent(const AreaSystemConfig& configuration);
|
|
AreaSystemComponent() = default;
|
|
~AreaSystemComponent() = default;
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// AZ::Component interface implementation
|
|
void Init() override;
|
|
void Activate() override;
|
|
void Deactivate() override;
|
|
bool ReadInConfig(const AZ::ComponentConfig* baseConfig) override;
|
|
bool WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const override;
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// AreaSystemRequestBus
|
|
void RegisterArea(AZ::EntityId areaId, AZ::u32 layer, AZ::u32 priority, const AZ::Aabb& bounds) override;
|
|
void UnregisterArea(AZ::EntityId areaId) override;
|
|
void RefreshArea(AZ::EntityId areaId, AZ::u32 layer, AZ::u32 priority, const AZ::Aabb& bounds) override;
|
|
void RefreshAllAreas() override;
|
|
void ClearAllAreas() override;
|
|
void MuteArea(AZ::EntityId areaId) override;
|
|
void UnmuteArea(AZ::EntityId areaId) override;
|
|
void EnumerateInstancesInOverlappingSectors(const AZ::Aabb& bounds, AreaSystemEnumerateCallback callback) const override;
|
|
void EnumerateInstancesInAabb(const AZ::Aabb& bounds, AreaSystemEnumerateCallback callback) const override;
|
|
AZStd::size_t GetInstanceCountInAabb(const AZ::Aabb& bounds) const override;
|
|
AZStd::vector<Vegetation::InstanceData> GetInstancesInAabb(const AZ::Aabb& bounds) const override;
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// GradientSignal::SectorDataRequestBus
|
|
void GetPointsPerMeter(float& value) const override;
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// AZ::TickBus
|
|
void OnTick(float deltaTime, AZ::ScriptTimePoint time) override;
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// SystemConfigurationRequestBus
|
|
void UpdateSystemConfig(const AZ::ComponentConfig* config) override;
|
|
void GetSystemConfig(AZ::ComponentConfig* config) const override;
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// SurfaceData::SurfaceDataSystemNotificationBus
|
|
void OnSurfaceChanged(const AZ::EntityId& entityId, const AZ::Aabb& oldBounds, const AZ::Aabb& newBounds) override;
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// InstanceStatObjEventBus
|
|
void ReleaseData() override;
|
|
|
|
////////////////////////////////////////////////////////////////////////////
|
|
// CrySystemEvents
|
|
void OnCrySystemInitialized(ISystem& system, const SSystemInitParams& systemInitParams) override;
|
|
void OnCrySystemShutdown(ISystem& system) override;
|
|
void OnCryEditorBeginLevelExport() override;
|
|
void OnCryEditorEndLevelExport(bool /*success*/) override;
|
|
void OnCryEditorCloseScene() override;
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// ISystemEventListener
|
|
void OnSystemEvent(ESystemEvent event, UINT_PTR wparam, UINT_PTR lparam) override;
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// TerrainDataNotificationBus
|
|
void OnTerrainDataCreateBegin() override;
|
|
void OnTerrainDataDestroyBegin() override;
|
|
|
|
private:
|
|
using ClaimContainer = AZStd::unordered_map<ClaimHandle, InstanceData>;
|
|
using ClaimContainerEntry = AZStd::pair<ClaimHandle, InstanceData>;
|
|
|
|
using SectorId = AZStd::pair<int, int>;
|
|
|
|
// SectorInfo contains basic sector information and the set of "plantable points" in the sector that have been claimed
|
|
struct SectorInfo
|
|
{
|
|
AZ_CLASS_ALLOCATOR(SectorInfo, AZ::SystemAllocator, 0);
|
|
|
|
SectorId m_id = {};
|
|
AZ::Aabb m_bounds = {};
|
|
//! Keeps track of points that have been claimed. This is not cleared at the start of an update pass
|
|
ClaimContainer m_claimedWorldPoints;
|
|
//! Keeps track of previous state of sector while filling to avoid redundant instance destroy/create calls
|
|
ClaimContainer m_claimedWorldPointsBeforeFill;
|
|
ClaimContext m_baseContext;
|
|
|
|
int GetSectorX() const { return m_id.first; }
|
|
int GetSectorY() const { return m_id.second; }
|
|
};
|
|
|
|
// VegetationAreaInfo contains the basic information we need for tracking which vegetation areas to apply
|
|
// to which sectors, and in which order.
|
|
struct VegetationAreaInfo
|
|
{
|
|
AZ_CLASS_ALLOCATOR(VegetationAreaInfo, AZ::SystemAllocator, 0);
|
|
|
|
AZ::EntityId m_id;
|
|
AZ::Aabb m_bounds = {};
|
|
AZ::u32 m_layer = {};
|
|
AZ::u32 m_priority = {};
|
|
};
|
|
using VegetationAreaMap = AZStd::unordered_map<AZ::EntityId, VegetationAreaInfo>;
|
|
using VegetationAreaSet = AZStd::unordered_set<AZ::EntityId>;
|
|
using VegetationAreaVector = AZStd::vector<VegetationAreaInfo>;
|
|
using UnregisteredVegetationAreaMap = AZStd::unordered_map<SectorId, AZStd::unordered_set<AZ::EntityId>>;
|
|
|
|
//! Helper class to track whether or not a visible sector is dirty. Different instances of this
|
|
//! class are used to track different reasons for being dirty.
|
|
//! This is a class instead of just an unordered_set<> so that we can also encapsulate the optimization
|
|
//! of tracking when *all* sectors are dirty.
|
|
class DirtySectors
|
|
{
|
|
public:
|
|
DirtySectors() = default;
|
|
~DirtySectors() = default;
|
|
|
|
void MarkDirty(const SectorId& id);
|
|
void MarkAllDirty();
|
|
bool IsAllDirty() const { return m_allSectorsDirty; }
|
|
bool IsNoneDirty() const { return (!m_allSectorsDirty) && m_dirtySet.empty(); }
|
|
bool IsDirty(const SectorId& id) const;
|
|
void Clear();
|
|
|
|
private:
|
|
using DirtySectorSet = AZStd::unordered_set<SectorId>;
|
|
DirtySectorSet m_dirtySet;
|
|
//! Flag when *all* existing sectors are dirty
|
|
bool m_allSectorsDirty = false;
|
|
};
|
|
|
|
|
|
// ViewRect is a helper struct to manage the "scrolling view rectangle". This view rectangle controls the
|
|
// set of active spawned vegetation.
|
|
struct ViewRect
|
|
{
|
|
int m_x = 0;
|
|
int m_y = 0;
|
|
int m_width = 0;
|
|
int m_height = 0;
|
|
AZ::Aabb m_viewRectBounds = AZ::Aabb::CreateNull();
|
|
|
|
ViewRect() = default;
|
|
|
|
ViewRect(int inX, int inY, int inW, int inH, AZ::Aabb viewRectBounds)
|
|
: m_x(inX)
|
|
, m_y(inY)
|
|
, m_width(inW)
|
|
, m_height(inH)
|
|
, m_viewRectBounds(viewRectBounds)
|
|
{
|
|
}
|
|
|
|
bool IsInside(const SectorId& sector) const;
|
|
ViewRect Overlap(const ViewRect& b) const;
|
|
bool operator !=(const ViewRect& b);
|
|
bool operator ==(const ViewRect& b);
|
|
size_t GetNumSectors() const;
|
|
int GetMinXSector() const { return m_x; }
|
|
int GetMinYSector() const { return m_y; }
|
|
int GetMaxXSector() const { return m_x + m_width - 1; }
|
|
int GetMaxYSector() const { return m_y + m_height - 1; }
|
|
SectorId GetMinSector() const { return SectorId(GetMinXSector(), GetMinYSector()); }
|
|
SectorId GetMaxSector() const { return SectorId(GetMaxXSector(), GetMaxYSector()); }
|
|
AZ::Aabb GetViewRectBounds() const { return m_viewRectBounds; }
|
|
};
|
|
|
|
// Forward declarations, these get defined further down.
|
|
class UpdateContext;
|
|
class PersistentThreadData;
|
|
|
|
// Thread-local copies of main state. We make copies of this to ensure that we can process sectors safely on
|
|
// the vegetation thread while these values potentially get changed on the main thread without needing to wrap
|
|
// all access with mutexes.
|
|
struct CachedMainThreadData
|
|
{
|
|
float m_worldToSector = 0.0f;
|
|
ViewRect m_currViewRect = {};
|
|
int m_sectorSizeInMeters = 0;
|
|
int m_sectorDensity = 0;
|
|
SnapMode m_sectorPointSnapMode = SnapMode::Corner;
|
|
};
|
|
|
|
// VegetationThreadTasks is the task queue that's used equally by the main thread and the vegetation thread.
|
|
// The main thread generally queues the tasks, and the vegetation thread processes them.
|
|
// Any data used from this class requires mutexes, atomics, or other thread protection.
|
|
class VegetationThreadTasks
|
|
{
|
|
public:
|
|
void QueueVegetationTask(AZStd::function<void(UpdateContext* context, PersistentThreadData* threadData, VegetationThreadTasks * vegTasks)> func);
|
|
void ProcessVegetationThreadTasks(UpdateContext* context, PersistentThreadData* threadData);
|
|
bool VegetationThreadTasksPending()
|
|
{
|
|
AZStd::lock_guard<decltype(m_vegetationThreadTaskMutex)> lock(m_vegetationThreadTaskMutex);
|
|
return !m_vegetationThreadTasks.empty();
|
|
}
|
|
|
|
//! Get sector by 2d veg map coordinates.
|
|
const SectorInfo* GetSector(const SectorId& sectorId) const;
|
|
SectorInfo* GetSector(const SectorId& sectorId);
|
|
|
|
SectorInfo* CreateSector(const SectorId& sectorId, int sectorDensity, int sectorSizeInMeters, SnapMode sectorPointSnapMode);
|
|
void UpdateSectorPoints(SectorInfo& sectorInfo, int sectorDensity, int sectorSizeInMeters, SnapMode sectorPointSnapMode);
|
|
void FillSector(SectorInfo& sectorInfo, const VegetationAreaVector& activeAreas);
|
|
void DeleteSector(const SectorId& sectorId);
|
|
void ClearSectors();
|
|
|
|
//! Gets the AABB for a sector
|
|
static AZ::Aabb GetSectorBounds(const SectorId& sectorId, int sectorSizeInMeters);
|
|
|
|
void FetchDebugData();
|
|
|
|
void MarkDirtySectors(const AZ::Aabb& bounds, DirtySectors& dirtySet, float worldToSector, const ViewRect& viewRect);
|
|
void AddUnregisteredVegetationArea(const VegetationAreaInfo& area, float worldToSector, const ViewRect& viewRect);
|
|
|
|
//! 2D Array rolling window of sectors that store vegetation objects.
|
|
using SectorRollingWindow = AZStd::unordered_map<SectorId, SectorInfo>;
|
|
mutable AZStd::recursive_mutex m_sectorRollingWindowMutex;
|
|
SectorRollingWindow m_sectorRollingWindow;
|
|
|
|
private:
|
|
// claiming logic
|
|
void CreateClaim(SectorInfo& sectorInfo, const ClaimHandle handle, const InstanceData& instanceData);
|
|
ClaimHandle CreateClaimHandle(const SectorInfo& sectorInfo, uint32_t index) const;
|
|
|
|
void ReleaseUnusedClaims(SectorInfo& sectorInfo);
|
|
void ReleaseUnregisteredClaims(SectorInfo& sectorInfo);
|
|
|
|
//! Creates a new sector
|
|
void UpdateSectorCallbacks(SectorInfo& sectorInfo);
|
|
|
|
static void EmptySector(SectorInfo& sectorInfo);
|
|
|
|
// Calls the given function on each sector in the box
|
|
template<class Fn>
|
|
static void EnumerateSectorsInAabb(const AZ::Aabb& bounds, float worldToSector, const ViewRect& viewRect, Fn&& fn);
|
|
|
|
|
|
//! Queued list of vegetation area state update requests. These get queued on the main thread, and processed
|
|
//! on the vegetation thread.
|
|
mutable AZStd::recursive_mutex m_vegetationThreadTaskMutex;
|
|
using VegetationThreadTaskList = AZStd::list<AZStd::function<void(UpdateContext* context, PersistentThreadData* threadData, VegetationThreadTasks* vegTasks)>>;
|
|
VegetationThreadTaskList m_vegetationThreadTasks;
|
|
|
|
//! Map from sectors to areas affecting that sector which have been unregistered and need to have their claims released
|
|
//! Note: This is only updated from the vegetation thread when processing vegetation tasks.
|
|
UnregisteredVegetationAreaMap m_unregisteredVegetationAreaSet;
|
|
|
|
//! Cached pointer to the debug data.
|
|
//! Note: This doesn't have an associated mutex because DebugData itself consists purely of atomics
|
|
DebugData* m_debugData = nullptr;
|
|
};
|
|
|
|
//! Helper struct to hold the state data used by the vegetation thread. This contains all the data
|
|
//! that should persist between thread runs, which lets us completely shut down the thread when there's
|
|
//! no work to do.
|
|
//! This also contains the vegetation thread mutex and state variables, which are accessed by both threads
|
|
//! to manage synchronization.
|
|
class PersistentThreadData
|
|
{
|
|
public:
|
|
// This mutex is active the entire time the vegetation thread is running. Its main purpose
|
|
// is to ensure we don't have component activations / deactivations that occur while the vegetation
|
|
// thread is still processing.
|
|
mutable AZStd::recursive_mutex m_vegetationThreadMutex;
|
|
|
|
// Current state of the vegetation thread
|
|
enum class VegetationThreadState
|
|
{
|
|
Stopped,
|
|
Running,
|
|
InterruptRequested
|
|
};
|
|
AZStd::atomic<VegetationThreadState> m_vegetationThreadState{ VegetationThreadState::Stopped };
|
|
|
|
// Current state of data synchronization between main thread and vegetation thread
|
|
enum class VegetationDataSyncState
|
|
{
|
|
Synchronized,
|
|
Dirty,
|
|
Updating
|
|
};
|
|
AZStd::atomic<VegetationDataSyncState> m_vegetationDataSyncState{ VegetationDataSyncState::Synchronized };
|
|
|
|
//! Reset the states that can get recalculated when the vegetation thread is run.
|
|
//! This does *not* reset the states on registered vegetation area lists, since those only
|
|
//! get filled out once.
|
|
void Init()
|
|
{
|
|
m_activeAreasDirty = true;
|
|
m_activeAreas.clear();
|
|
m_activeAreasInBubble.clear();
|
|
m_dirtySectorContents.Clear();
|
|
m_dirtySectorSurfacePoints.Clear();
|
|
}
|
|
|
|
void InterruptVegetationThread();
|
|
|
|
// The following state is public because it's accessed by queued vegetation tasks and UpdateContext. It should
|
|
// eventually be encapsulated a little better.
|
|
|
|
//! set of sectors that need their contents refreshed
|
|
DirtySectors m_dirtySectorContents;
|
|
//! set of sectors that need their surface points recalculated (which implies also needing the contents refreshed)
|
|
DirtySectors m_dirtySectorSurfacePoints;
|
|
|
|
VegetationAreaMap m_globalVegetationAreaMap;
|
|
VegetationAreaSet m_ignoredVegetationAreaSet;
|
|
|
|
//! Determines when to refresh the set of active areas
|
|
bool m_activeAreasDirty = true;
|
|
|
|
protected:
|
|
// Only UpdateContext is allowed to directly access the persisted thread state.
|
|
friend class UpdateContext;
|
|
|
|
// This is effectively a local variable in UpdateActiveVegetationAreas, but is kept
|
|
// persistent to avoid potentially frequent reallocation.
|
|
VegetationAreaVector m_activeAreas;
|
|
|
|
//! The set of active vegetation areas that overlap the current view rectangle
|
|
VegetationAreaVector m_activeAreasInBubble;
|
|
};
|
|
|
|
|
|
// UpdateContext is the logic that normally runs on the vegetation thread. It processes all of the logic needed to update and fill
|
|
// any vegetation sectors that are currently within the view rectangle. Occasionally the logic will be triggered on the main thread
|
|
// in cases such as shutdown when the vegetation thread isn't running any we need to perform the cleanup synchronously.
|
|
class UpdateContext
|
|
{
|
|
public:
|
|
UpdateContext() = default;
|
|
|
|
void Run(PersistentThreadData* threadData, VegetationThreadTasks* vegTasks, CachedMainThreadData* cachedMainThreadData);
|
|
void UpdateActiveVegetationAreas(PersistentThreadData* threadData, const ViewRect& viewRect);
|
|
const CachedMainThreadData& GetCachedMainThreadData() { return m_cachedMainThreadData; }
|
|
|
|
private:
|
|
bool UpdateSectorWorkLists(PersistentThreadData* threadData, VegetationThreadTasks* vegTasks);
|
|
bool UpdateOneSector(PersistentThreadData* threadData, VegetationThreadTasks* vegTasks);
|
|
|
|
enum class UpdateMode
|
|
{
|
|
Create,
|
|
RebuildSurfaceCacheAndFill,
|
|
Fill
|
|
};
|
|
|
|
// The sorted work list of sectors to delete. The list is recreated every time UpdateSectorWorkLists() is run.
|
|
AZStd::vector<SectorId> m_deleteWorkList;
|
|
|
|
// The sorted work list of sectors to create / update. This is incrementally modified when UpdateSectorWorkLists()
|
|
// is run, because any previously-requested updates that are still in view need to be preserved. They can't simply
|
|
// be recalculated.
|
|
AZStd::vector<AZStd::pair<SectorId, UpdateMode>> m_updateWorkList;
|
|
|
|
// Sector counts of the number of expected sectors in the view rectangle vs the number of sectors
|
|
// currently active. These are used to "load balance" sector deletes and creates so that we don't have
|
|
// too many sectors active at any one point in time.
|
|
size_t m_viewRectSectorCount = 0;
|
|
|
|
// Thread-local copy of the main thread's m_cachedMainThreadData. This way we can read from it on the vegetation
|
|
// thread without requiring mutexes.
|
|
CachedMainThreadData m_cachedMainThreadData;
|
|
|
|
};
|
|
|
|
bool ApplyPendingConfigChanges();
|
|
|
|
void ReleaseAllClaims();
|
|
void ReleaseWithoutCleanup();
|
|
|
|
bool CalculateViewRect();
|
|
|
|
|
|
//! Get sector id by world coordinates.
|
|
static SectorId GetSectorId(const AZ::Vector3& worldPos, float worldToSector);
|
|
|
|
|
|
// All of this state data should only get accessed by the main thread. A subset of this data gets copied
|
|
// into CachedMainThreadData for the vegetation thread to be able to query in a lockless manner.
|
|
AreaSystemConfig m_configuration;
|
|
float m_worldToSector = 0.0f; //! world to sector scaling ratio.
|
|
ViewRect m_currViewRect = {};
|
|
float m_vegetationThreadTaskTimer = 0.0f;
|
|
ISystem* m_system = nullptr;
|
|
bool m_configDirty = false;
|
|
AreaSystemConfig m_pendingConfigUpdate;
|
|
|
|
// The vegetation task queue gets read/written from both threads, and uses atomics + mutexes for synchronization.
|
|
VegetationThreadTasks m_vegTasks;
|
|
|
|
// This state should only get read or written from the vegetation thread, except for component initialization.
|
|
PersistentThreadData m_threadData;
|
|
|
|
// This state gets written to from the main thread, and gets copied and read from the vegetation thread.
|
|
CachedMainThreadData m_cachedMainThreadData;
|
|
};
|
|
}
|