Files
o3de/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp
T
Mike Balfour 089391f761 Terrain System cleanups and unit tests (#4119)
* Remove the "TEST_SUPPORTED" traits.
Terrain unit tests should be usable on all platforms, so they shouldn't need a platform-specific trait to enable/disable.

Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com>

* Fix a few misc terrain bugs.
* Change Activate/Deactivate to happen immediately instead of deferring.  There were too many order-of-operation bugs caused by trying to defer this.
* Added implementation for calculating normals.
* Fixed bug where GetHeightSynchronous wasn't stopping at the highest-priority layer.
* Added locks for SurfaceData bus to help ensure we lock our mutexes in the correct order and avoid deadlocks.

Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com>

* Add trivial TerrainSystem tests.
Tests construction, Activate(), Deactivate(), and destruction.

Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com>

* Unified Terrain system calls on single bus.

Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com>

* Added mock for TerrainDataNotificationBus listener.
Also added unit tests to verify the listener, and added in missing notification events.

Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com>

* Removed extra Sampler class.
Fixed up APIs to correctly pass Sampler and terrainExistsPtr around.

Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com>

* Switched MockTerrainSystem to be proper gmock.
This makes it for flexible to use and easier to reuse from other test environments.

Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com>

* Fix settings bug caused by bad order of operations that occurred when the methods moved to a different bus.

Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com>

* Eliminate extra EBus by simplifying area initialization.
Previously, there was a back-and-forth ebus signal used for the terrain system to find any terrain spawners that were created prior to the terrain system activation.  Now it uses the more simple technique of just grabbing all the spawners that are currently hooked up to the spawner ebus.

Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com>

* Switch to NiceMock so that "uninteresting" mock calls get ignored.

Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com>

* Addressed PR feedback.
Filled in terrainExistsPtr at the end, and added it to GetNormal as well.

Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com>

* Fixed shader height calculation.
It was off by half a pixel, and it was interpolating, both of which were wrong.

Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com>
2021-09-15 10:57:37 -05:00

548 lines
20 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 <TerrainSystem/TerrainSystem.h>
#include <AzCore/std/parallel/shared_mutex.h>
#include <SurfaceData/SurfaceDataTypes.h>
#include <SurfaceData/SurfaceDataSystemRequestBus.h>
#include <LmbrCentral/Shape/ShapeComponentBus.h>
#include <Atom/RPI.Public/Scene.h>
#include <Atom/RPI.Public/FeatureProcessorFactory.h>
#include <TerrainRenderer/TerrainFeatureProcessor.h>
using namespace Terrain;
bool TerrainLayerPriorityComparator::operator()(const AZ::EntityId& layer1id, const AZ::EntityId& layer2id) const
{
// Comparator for insertion/keylookup.
// Sorts into layer/priority order, highest priority first.
AZ::u32 priority1, layer1;
Terrain::TerrainSpawnerRequestBus::Event(layer1id, &Terrain::TerrainSpawnerRequestBus::Events::GetPriority, layer1, priority1);
AZ::u32 priority2, layer2;
Terrain::TerrainSpawnerRequestBus::Event(layer2id, &Terrain::TerrainSpawnerRequestBus::Events::GetPriority, layer2, priority2);
if (layer1 < layer2)
{
return false;
}
else if (layer1 > layer2)
{
return true;
}
if (priority1 != priority2)
{
return priority1 > priority2;
}
return layer1id > layer2id;
}
TerrainSystem::TerrainSystem()
{
Terrain::TerrainSystemServiceRequestBus::Handler::BusConnect();
AZ::TickBus::Handler::BusConnect();
m_currentSettings.m_systemActive = false;
m_currentSettings.m_worldBounds = AZ::Aabb::CreateNull();
m_requestedSettings = m_currentSettings;
m_requestedSettings.m_worldBounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-512.0f), AZ::Vector3(512.0f));
}
TerrainSystem::~TerrainSystem()
{
AZ::TickBus::Handler::BusDisconnect();
Terrain::TerrainSystemServiceRequestBus::Handler::BusDisconnect();
Deactivate();
}
void TerrainSystem::Activate()
{
AzFramework::Terrain::TerrainDataNotificationBus::Broadcast(
&AzFramework::Terrain::TerrainDataNotificationBus::Events::OnTerrainDataCreateBegin);
m_dirtyRegion = AZ::Aabb::CreateNull();
m_terrainHeightDirty = true;
m_terrainSettingsDirty = true;
m_requestedSettings.m_systemActive = true;
{
AZStd::shared_lock<AZStd::shared_mutex> lock(m_areaMutex);
m_registeredAreas.clear();
}
AzFramework::Terrain::TerrainDataRequestBus::Handler::BusConnect();
// Register any terrain spawners that were already active before the terrain system activated.
auto enumerationCallback = [&]([[maybe_unused]] Terrain::TerrainSpawnerRequests* terrainSpawner) -> bool
{
AZ::EntityId areaId = *(Terrain::TerrainSpawnerRequestBus::GetCurrentBusId());
RegisterArea(areaId);
// Keep Enumerating
return true;
};
Terrain::TerrainSpawnerRequestBus::EnumerateHandlers(enumerationCallback);
AzFramework::Terrain::TerrainDataNotificationBus::Broadcast(
&AzFramework::Terrain::TerrainDataNotificationBus::Events::OnTerrainDataCreateEnd);
}
void TerrainSystem::Deactivate()
{
AzFramework::Terrain::TerrainDataNotificationBus::Broadcast(
&AzFramework::Terrain::TerrainDataNotificationBus::Events::OnTerrainDataDestroyBegin);
AzFramework::Terrain::TerrainDataRequestBus::Handler::BusDisconnect();
{
AZStd::shared_lock<AZStd::shared_mutex> lock(m_areaMutex);
m_registeredAreas.clear();
}
m_dirtyRegion = AZ::Aabb::CreateNull();
m_terrainHeightDirty = true;
m_terrainSettingsDirty = true;
m_requestedSettings.m_systemActive = false;
if (auto rpi = AZ::RPI::RPISystemInterface::Get(); rpi)
{
if (auto defaultScene = rpi->GetDefaultScene(); defaultScene)
{
const AZ::RPI::Scene* scene = defaultScene.get();
if (auto terrainFeatureProcessor = scene->GetFeatureProcessor<TerrainFeatureProcessor>(); terrainFeatureProcessor)
{
terrainFeatureProcessor->RemoveTerrainData();
}
}
}
AzFramework::Terrain::TerrainDataNotificationBus::Broadcast(
&AzFramework::Terrain::TerrainDataNotificationBus::Events::OnTerrainDataDestroyEnd);
}
void TerrainSystem::SetTerrainAabb(const AZ::Aabb& worldBounds)
{
m_requestedSettings.m_worldBounds = worldBounds;
m_terrainSettingsDirty = true;
}
void TerrainSystem::SetTerrainHeightQueryResolution(AZ::Vector2 queryResolution)
{
m_requestedSettings.m_heightQueryResolution = queryResolution;
m_terrainSettingsDirty = true;
}
AZ::Aabb TerrainSystem::GetTerrainAabb() const
{
return m_currentSettings.m_worldBounds;
}
AZ::Vector2 TerrainSystem::GetTerrainHeightQueryResolution() const
{
return m_currentSettings.m_heightQueryResolution;
}
float TerrainSystem::GetHeightSynchronous(float x, float y, Sampler sampler, bool* terrainExistsPtr) const
{
bool terrainExists = false;
AZ::Vector3 inPosition((float)x, (float)y, m_currentSettings.m_worldBounds.GetMin().GetZ());
AZ::Vector3 outPosition((float)x, (float)y, m_currentSettings.m_worldBounds.GetMin().GetZ());
AZStd::shared_lock<AZStd::shared_mutex> lock(m_areaMutex);
for (auto& [areaId, areaBounds] : m_registeredAreas)
{
inPosition.SetZ(areaBounds.GetMin().GetZ());
if (areaBounds.Contains(inPosition))
{
Terrain::TerrainAreaHeightRequestBus::Event(
areaId, &Terrain::TerrainAreaHeightRequestBus::Events::GetHeight, inPosition, outPosition, sampler);
terrainExists = true;
break;
}
}
if (terrainExistsPtr)
{
*terrainExistsPtr = terrainExists;
}
return AZ::GetClamp(
outPosition.GetZ(), m_currentSettings.m_worldBounds.GetMin().GetZ(), m_currentSettings.m_worldBounds.GetMax().GetZ());
}
float TerrainSystem::GetHeight(AZ::Vector3 position, Sampler sampler, bool* terrainExistsPtr) const
{
return GetHeightSynchronous(position.GetX(), position.GetY(), sampler, terrainExistsPtr);
}
float TerrainSystem::GetHeightFromFloats(float x, float y, Sampler sampler, bool* terrainExistsPtr) const
{
return GetHeightSynchronous(x, y, sampler, terrainExistsPtr);
}
bool TerrainSystem::GetIsHoleFromFloats(float x, float y, Sampler sampler) const
{
bool terrainExists = false;
GetHeightSynchronous(x, y, sampler, &terrainExists);
return !terrainExists;
}
AZ::Vector3 TerrainSystem::GetNormalSynchronous(float x, float y, Sampler sampler, bool* terrainExistsPtr) const
{
bool terrainExists = false;
AZ::Vector3 inPosition((float)x, (float)y, m_currentSettings.m_worldBounds.GetMin().GetZ());
AZ::Vector3 outNormal = AZ::Vector3::CreateAxisZ();
AZStd::shared_lock<AZStd::shared_mutex> lock(m_areaMutex);
for (auto& [areaId, areaBounds] : m_registeredAreas)
{
inPosition.SetZ(areaBounds.GetMin().GetZ());
if (areaBounds.Contains(inPosition))
{
Terrain::TerrainAreaHeightRequestBus::Event(
areaId, &Terrain::TerrainAreaHeightRequestBus::Events::GetNormal, inPosition, outNormal, sampler);
terrainExists = true;
break;
}
}
if (terrainExistsPtr)
{
*terrainExistsPtr = terrainExists;
}
return outNormal;
}
AZ::Vector3 TerrainSystem::GetNormal(AZ::Vector3 position, Sampler sampler, bool* terrainExistsPtr) const
{
return GetNormalSynchronous(position.GetX(), position.GetY(), sampler, terrainExistsPtr);
}
AZ::Vector3 TerrainSystem::GetNormalFromFloats(float x, float y, Sampler sampler, bool* terrainExistsPtr) const
{
return GetNormalSynchronous(x, y, sampler, terrainExistsPtr);
}
AzFramework::SurfaceData::SurfaceTagWeight TerrainSystem::GetMaxSurfaceWeight(
[[maybe_unused]] AZ::Vector3 position, [[maybe_unused]] Sampler sampleFilter, [[maybe_unused]] bool* terrainExistsPtr) const
{
if (terrainExistsPtr)
{
*terrainExistsPtr = true;
}
return AzFramework::SurfaceData::SurfaceTagWeight();
}
AzFramework::SurfaceData::SurfaceTagWeight TerrainSystem::GetMaxSurfaceWeightFromFloats(
[[maybe_unused]] float x,
[[maybe_unused]] float y,
[[maybe_unused]] Sampler sampleFilter,
[[maybe_unused]] bool* terrainExistsPtr) const
{
if (terrainExistsPtr)
{
*terrainExistsPtr = true;
}
return AzFramework::SurfaceData::SurfaceTagWeight();
}
const char* TerrainSystem::GetMaxSurfaceName(
[[maybe_unused]] AZ::Vector3 position, [[maybe_unused]] Sampler sampleFilter, [[maybe_unused]] bool* terrainExistsPtr) const
{
if (terrainExistsPtr)
{
*terrainExistsPtr = true;
}
return "";
}
/*
void TerrainSystem::GetSurfaceWeights(
[[maybe_unused]] const AZ::Vector3& inPosition,
[[maybe_unused]] Sampler sampleFilter,
[[maybe_unused]] SurfaceData::SurfaceTagWeightMap& outSurfaceWeights)
{
// TODO: implement
}
void TerrainSystem::GetSurfacePoint(
const AZ::Vector3& inPosition, [[maybe_unused]] Sampler sampleFilter, SurfaceData::SurfacePoint& outSurfacePoint)
{
// TODO: Handle sampleFilter
float sampleX = inPosition.GetX();
float sampleY = inPosition.GetY();
GetHeight(inPosition, sampleFilter, outSurfacePoint.m_position);
//outSurfacePoint.m_position = AZ::Vector3(sampleX, sampleY, GetHeightSynchronous(sampleX, sampleY));
outSurfacePoint.m_normal = GetNormalSynchronous(sampleX, sampleY);
}
void TerrainSystem::ProcessHeightsFromRegion(const AZ::Aabb& inRegion, const AZ::Vector2 stepSize, Sampler sampleFilter, SurfacePointRegionFillCallback perPositionCallback, TerrainDataReadyCallback onComplete)
{
// Don't bother processing if we don't have a callback
if (!perPositionCallback)
{
return;
}
uint32_t numSamplesX = static_cast<uint32_t>((inRegion.GetMax().GetX() - inRegion.GetMin().GetX()) / stepSize.GetX());
uint32_t numSamplesY = static_cast<uint32_t>((inRegion.GetMax().GetY() - inRegion.GetMin().GetY()) / stepSize.GetY());
for (uint32_t y = 0; y < numSamplesY; y++)
{
for (uint32_t x = 0; x < numSamplesX; x++)
{
float fx = (float)(inRegion.GetMin().GetX() + (x * stepSize.GetX()));
float fy = (float)(inRegion.GetMin().GetY() + (y * stepSize.GetY()));
SurfaceData::SurfacePoint surfacePoint;
GetHeight(AZ::Vector3(fx, fy, 0.0f), sampleFilter, surfacePoint.m_position);
perPositionCallback(surfacePoint, x, y);
}
}
if (onComplete)
{
onComplete();
}
}
void TerrainSystem::ProcessSurfacePointsFromRegion(const AZ::Aabb& inRegion, const AZ::Vector2 stepSize, Sampler sampleFilter, SurfacePointRegionFillCallback perPositionCallback, TerrainDataReadyCallback onComplete)
{
// Don't bother processing if we don't have a callback
if (!perPositionCallback)
{
return;
}
uint32_t numSamplesX = static_cast<uint32_t>((inRegion.GetMax().GetX() - inRegion.GetMin().GetX()) / stepSize.GetX());
uint32_t numSamplesY = static_cast<uint32_t>((inRegion.GetMax().GetY() - inRegion.GetMin().GetY()) / stepSize.GetY());
for (uint32_t y = 0; y < numSamplesY; y++)
{
for (uint32_t x = 0; x < numSamplesX; x++)
{
float fx = (float)(inRegion.GetMin().GetX() + (x * stepSize.GetX()));
float fy = (float)(inRegion.GetMin().GetY() + (y * stepSize.GetY()));
SurfaceData::SurfacePoint surfacePoint;
GetSurfacePoint(AZ::Vector3(fx, fy, inRegion.GetMin().GetZ()), sampleFilter, surfacePoint);
perPositionCallback(surfacePoint, x, y);
}
}
if (onComplete)
{
onComplete();
}
}
*/
void TerrainSystem::RegisterArea(AZ::EntityId areaId)
{
AZStd::unique_lock<AZStd::shared_mutex> lock(m_areaMutex);
AZ::Aabb aabb = AZ::Aabb::CreateNull();
LmbrCentral::ShapeComponentRequestsBus::EventResult(aabb, areaId, &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb);
m_registeredAreas[areaId] = aabb;
m_dirtyRegion.AddAabb(aabb);
m_terrainHeightDirty = true;
}
void TerrainSystem::UnregisterArea(AZ::EntityId areaId)
{
AZStd::unique_lock<AZStd::shared_mutex> lock(m_areaMutex);
// Remove the data for this entity from the registered areas.
// Erase_if is used as erase would use the comparator to lookup the entity id in the map.
// As the comparator will get the new layer/priority data for the entity, the id lookup will fail.
AZStd::erase_if(
m_registeredAreas,
[areaId, this](const auto& item)
{
auto const& [entityId, aabb] = item;
if (areaId == entityId)
{
m_dirtyRegion.AddAabb(aabb);
m_terrainHeightDirty = true;
return true;
}
return false;
});
}
void TerrainSystem::RefreshArea(AZ::EntityId areaId)
{
AZStd::unique_lock<AZStd::shared_mutex> lock(m_areaMutex);
auto areaAabb = m_registeredAreas.find(areaId);
AZ::Aabb oldAabb = (areaAabb != m_registeredAreas.end()) ? areaAabb->second : AZ::Aabb::CreateNull();
AZ::Aabb newAabb = AZ::Aabb::CreateNull();
LmbrCentral::ShapeComponentRequestsBus::EventResult(newAabb, areaId, &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb);
m_registeredAreas[areaId] = newAabb;
AZ::Aabb expandedAabb = oldAabb;
expandedAabb.AddAabb(newAabb);
m_dirtyRegion.AddAabb(expandedAabb);
m_terrainHeightDirty = true;
}
void TerrainSystem::OnTick(float /*deltaTime*/, AZ::ScriptTimePoint /*time*/)
{
bool terrainSettingsChanged = false;
if (m_terrainSettingsDirty)
{
terrainSettingsChanged = true;
m_terrainSettingsDirty = false;
// This needs to happen before the "system active" check below, because activating the system will cause the various
// terrain layer areas to request the current world bounds.
if (m_requestedSettings.m_worldBounds != m_currentSettings.m_worldBounds)
{
m_dirtyRegion = m_currentSettings.m_worldBounds;
m_dirtyRegion.AddAabb(m_requestedSettings.m_worldBounds);
m_terrainHeightDirty = true;
m_currentSettings.m_worldBounds = m_requestedSettings.m_worldBounds;
}
if (m_requestedSettings.m_heightQueryResolution != m_currentSettings.m_heightQueryResolution)
{
m_dirtyRegion = AZ::Aabb::CreateNull();
m_terrainHeightDirty = true;
}
m_currentSettings = m_requestedSettings;
}
if (m_currentSettings.m_systemActive && m_terrainHeightDirty)
{
AZStd::shared_lock<AZStd::shared_mutex> lock(m_areaMutex);
// Block other threads from accessing the surface data bus while we are in GetValue (which may call into the SurfaceData bus).
// We lock our surface data mutex *before* checking / setting "isRequestInProgress" so that we prevent race conditions
// that create false detection of cyclic dependencies when multiple requests occur on different threads simultaneously.
// (One case where this was previously able to occur was in rapid updating of the Preview widget on the
// GradientSurfaceDataComponent in the Editor when moving the threshold sliders back and forth rapidly)
auto& surfaceDataContext = SurfaceData::SurfaceDataSystemRequestBus::GetOrCreateContext(false);
typename SurfaceData::SurfaceDataSystemRequestBus::Context::DispatchLockGuard scopeLock(surfaceDataContext.m_contextMutex);
AZ::Transform transform = AZ::Transform::CreateTranslation(m_currentSettings.m_worldBounds.GetCenter());
uint32_t width = aznumeric_cast<uint32_t>(
(float)m_currentSettings.m_worldBounds.GetXExtent() / m_currentSettings.m_heightQueryResolution.GetX());
uint32_t height = aznumeric_cast<uint32_t>(
(float)m_currentSettings.m_worldBounds.GetYExtent() / m_currentSettings.m_heightQueryResolution.GetY());
AZStd::vector<float> pixels;
pixels.resize_no_construct(width * height);
const uint32_t pixelDataSize = width * height * sizeof(float);
memset(pixels.data(), 0, pixelDataSize);
for (uint32_t y = 0; y < height; y++)
{
for (uint32_t x = 0; x < width; x++)
{
// Find the first terrain layer that covers this position. This will be the highest priority, so others can be ignored.
for (auto& [areaId, areaBounds] : m_registeredAreas)
{
AZ::Vector3 inPosition(
(x * m_currentSettings.m_heightQueryResolution.GetX()) + m_currentSettings.m_worldBounds.GetMin().GetX(),
(y * m_currentSettings.m_heightQueryResolution.GetY()) + m_currentSettings.m_worldBounds.GetMin().GetY(),
areaBounds.GetMin().GetZ());
if (!areaBounds.Contains(inPosition))
{
continue;
}
AZ::Vector3 outPosition;
const AzFramework::Terrain::TerrainDataRequestBus::Events::Sampler sampleFilter =
AzFramework::Terrain::TerrainDataRequestBus::Events::Sampler::DEFAULT;
Terrain::TerrainAreaHeightRequestBus::Event(
areaId, &Terrain::TerrainAreaHeightRequestBus::Events::GetHeight, inPosition, outPosition, sampleFilter);
pixels[(y * width) + x] = (outPosition.GetZ() - m_currentSettings.m_worldBounds.GetMin().GetZ()) /
m_currentSettings.m_worldBounds.GetExtents().GetZ();
break;
}
}
}
const AZ::RPI::Scene* scene = AZ::RPI::RPISystemInterface::Get()->GetDefaultScene().get();
auto terrainFeatureProcessor = scene->GetFeatureProcessor<TerrainFeatureProcessor>();
AZ_Assert(terrainFeatureProcessor, "Unable to find a TerrainFeatureProcessor.");
if (terrainFeatureProcessor)
{
terrainFeatureProcessor->UpdateTerrainData(
transform, m_currentSettings.m_worldBounds, m_currentSettings.m_heightQueryResolution.GetX(), width, height, pixels);
}
}
if (terrainSettingsChanged || m_terrainHeightDirty)
{
// Block other threads from accessing the surface data bus while we are in GetValue (which may call into the SurfaceData bus).
// We lock our surface data mutex *before* checking / setting "isRequestInProgress" so that we prevent race conditions
// that create false detection of cyclic dependencies when multiple requests occur on different threads simultaneously.
// (One case where this was previously able to occur was in rapid updating of the Preview widget on the
// GradientSurfaceDataComponent in the Editor when moving the threshold sliders back and forth rapidly)
auto& surfaceDataContext = SurfaceData::SurfaceDataSystemRequestBus::GetOrCreateContext(false);
typename SurfaceData::SurfaceDataSystemRequestBus::Context::DispatchLockGuard scopeLock(surfaceDataContext.m_contextMutex);
AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask changeMask =
AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask::None;
if (terrainSettingsChanged)
{
changeMask = static_cast<AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask>(
changeMask | AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask::Settings);
}
if (m_terrainHeightDirty)
{
changeMask = static_cast<AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask>(
changeMask | AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask::HeightData);
}
// Make sure to set these *before* calling OnTerrainDataChanged, since it's possible that subsystems reacting to that call will
// cause the data to become dirty again.
AZ::Aabb dirtyRegion = m_dirtyRegion;
m_terrainHeightDirty = false;
m_dirtyRegion = AZ::Aabb::CreateNull();
AzFramework::Terrain::TerrainDataNotificationBus::Broadcast(
&AzFramework::Terrain::TerrainDataNotificationBus::Events::OnTerrainDataChanged, dirtyRegion,
changeMask);
}
}