Files
o3de/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp
T
bosnichd f34d3a822e Add a ray cast API to the terrain system, implement it, and use it so entities can be placed on top of terrain. (#6895)
* Add a ray cast API to the terrain system, implement it, and use it so entities can be placed on top of terrain.

Still to do:
- Unit tests
- Profiling/benchmarks
- Optimization (if needed, may not be now after updating to use the iterative approach, but one option could be to use SIMD to ray cast against both triangles at once)

Signed-off-by: bosnichd <bosnichd@amazon.com>

* Fix typos.

Signed-off-by: bosnichd <bosnichd@amazon.com>

* Added a FindNearestIntersectionIterative to use in place of the first-pass FindnearestIntersectionRecursive attempt.

Signed-off-by: bosnichd <bosnichd@amazon.com>

* Remove some functions that are no longer being used.

Signed-off-by: bosnichd <bosnichd@amazon.com>

* Remove a unicode character from a comment to fix the validation build.

Signed-off-by: bosnichd <bosnichd@amazon.com>

* Remove another unicode character from a comment to fix the validation build.

Signed-off-by: bosnichd <bosnichd@amazon.com>

* Update to bail out as soon as t > 1.0f

Signed-off-by: bosnichd <bosnichd@amazon.com>
2022-01-24 13:47:44 -07:00

969 lines
35 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 <AzCore/std/sort.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>
#include <Terrain/Ebuses/TerrainAreaSurfaceRequestBus.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 = 0;
AZ::u32 layer1 = 0;
Terrain::TerrainSpawnerRequestBus::Event(layer1id, &Terrain::TerrainSpawnerRequestBus::Events::GetPriority, layer1, priority1);
AZ::u32 priority2 = 0;
AZ::u32 layer2 = 0;
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()
: m_terrainRaycastContext(*this)
{
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_terrainSurfacesDirty = true;
m_requestedSettings.m_systemActive = true;
{
AZStd::unique_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()
{
// Stop listening to the bus even before we signal DestroyBegin so that way any calls to the terrain system as a *result* of
// calling DestroyBegin will fail to reach the terrain system.
AzFramework::Terrain::TerrainDataRequestBus::Handler::BusDisconnect();
AzFramework::Terrain::TerrainDataNotificationBus::Broadcast(
&AzFramework::Terrain::TerrainDataNotificationBus::Events::OnTerrainDataDestroyBegin);
{
AZStd::unique_lock<AZStd::shared_mutex> lock(m_areaMutex);
m_registeredAreas.clear();
}
m_dirtyRegion = AZ::Aabb::CreateNull();
m_terrainHeightDirty = true;
m_terrainSettingsDirty = true;
m_terrainSurfacesDirty = true;
m_requestedSettings.m_systemActive = false;
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;
}
void TerrainSystem::ClampPosition(float x, float y, AZ::Vector2& outPosition, AZ::Vector2& normalizedDelta) const
{
// Given an input position, clamp the values to our terrain grid, where it will always go to the terrain grid point
// at a lower value, whether positive or negative. Ex: 3.3 -> 3, -3.3 -> -4
// Also, return the normalized delta as a value of [0-1) describing what fraction of a grid point the value moved.
// Scale the position by the query resolution, so that integer values represent exact steps on the grid,
// and fractional values are the amount in-between each grid point, in the range [0-1).
AZ::Vector2 normalizedPosition = AZ::Vector2(x, y) / m_currentSettings.m_heightQueryResolution;
normalizedDelta = AZ::Vector2(
normalizedPosition.GetX() - floor(normalizedPosition.GetX()), normalizedPosition.GetY() - floor(normalizedPosition.GetY()));
// Remove the fractional part, then scale back down into world space.
outPosition = (normalizedPosition - normalizedDelta) * m_currentSettings.m_heightQueryResolution;
}
bool TerrainSystem::InWorldBounds(float x, float y) const
{
const float zTestValue = m_currentSettings.m_worldBounds.GetMin().GetZ();
const AZ::Vector3 testValue{ x, y, zTestValue };
if (m_currentSettings.m_worldBounds.Contains(testValue))
{
return true;
}
return false;
}
float TerrainSystem::GetHeightSynchronous(float x, float y, Sampler sampler, bool* terrainExistsPtr) const
{
bool terrainExists = false;
float height = m_currentSettings.m_worldBounds.GetMin().GetZ();
if (!InWorldBounds(x, y))
{
if (terrainExistsPtr)
{
*terrainExistsPtr = terrainExists;
return height;
}
}
AZStd::shared_lock<AZStd::shared_mutex> lock(m_areaMutex);
switch (sampler)
{
// Get the value at the requested location, using the terrain grid to bilinear filter between sample grid points.
case AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR:
{
// pos0 contains one corner of our grid square, pos1 contains the opposite corner, and normalizedDelta is the fractional
// amount the position exists between those corners.
// Ex: (3.3, 4.4) would have a pos0 of (3, 4), a pos1 of (4, 5), and a delta of (0.3, 0.4).
AZ::Vector2 normalizedDelta;
AZ::Vector2 pos0;
ClampPosition(x, y, pos0, normalizedDelta);
const AZ::Vector2 pos1 = pos0 + m_currentSettings.m_heightQueryResolution;
const float heightX0Y0 = GetTerrainAreaHeight(pos0.GetX(), pos0.GetY(), terrainExists);
const float heightX1Y0 = GetTerrainAreaHeight(pos1.GetX(), pos0.GetY(), terrainExists);
const float heightX0Y1 = GetTerrainAreaHeight(pos0.GetX(), pos1.GetY(), terrainExists);
const float heightX1Y1 = GetTerrainAreaHeight(pos1.GetX(), pos1.GetY(), terrainExists);
const float heightXY0 = AZ::Lerp(heightX0Y0, heightX1Y0, normalizedDelta.GetX());
const float heightXY1 = AZ::Lerp(heightX0Y1, heightX1Y1, normalizedDelta.GetX());
height = AZ::Lerp(heightXY0, heightXY1, normalizedDelta.GetY());
}
break;
//! Clamp the input point to the terrain sample grid, then get the height at the given grid location.
case AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP:
{
AZ::Vector2 normalizedDelta;
AZ::Vector2 clampedPosition;
ClampPosition(x, y, clampedPosition, normalizedDelta);
height = GetTerrainAreaHeight(clampedPosition.GetX(), clampedPosition.GetY(), terrainExists);
}
break;
//! Directly get the value at the location, regardless of terrain sample grid density.
case AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT:
[[fallthrough]];
default:
height = GetTerrainAreaHeight(x, y, terrainExists);
break;
}
// For now, always set terrainExists to true, as we don't have a way to author data for terrain holes yet.
if (terrainExistsPtr)
{
*terrainExistsPtr = terrainExists;
}
return AZ::GetClamp(
height, m_currentSettings.m_worldBounds.GetMin().GetZ(), m_currentSettings.m_worldBounds.GetMax().GetZ());
}
float TerrainSystem::GetTerrainAreaHeight(float x, float y, bool& terrainExists) const
{
const float worldMin = m_currentSettings.m_worldBounds.GetMin().GetZ();
AZ::Vector3 inPosition(x, y, worldMin);
float height = worldMin;
terrainExists = false;
AZStd::shared_lock<AZStd::shared_mutex> lock(m_areaMutex);
for (auto& [areaId, areaData] : m_registeredAreas)
{
const float areaMin = areaData.m_areaBounds.GetMin().GetZ();
inPosition.SetZ(areaMin);
if (areaData.m_areaBounds.Contains(inPosition))
{
AZ::Vector3 outPosition;
Terrain::TerrainAreaHeightRequestBus::Event(
areaId, &Terrain::TerrainAreaHeightRequestBus::Events::GetHeight, inPosition, outPosition, terrainExists);
height = outPosition.GetZ();
if (!terrainExists)
{
// If the terrain height provider doesn't have any data, then check the area's "use ground plane" setting.
// If it's set, then create a default ground plane by saying terrain exists at the minimum height for the area.
// Otherwise, we'll set the height at the terrain world minimum and say it doesn't exist.
terrainExists = areaData.m_useGroundPlane;
height = areaData.m_useGroundPlane ? areaMin : worldMin;
}
break;
}
}
return height;
}
float TerrainSystem::GetHeight(const AZ::Vector3& position, Sampler sampler, bool* terrainExistsPtr) const
{
return GetHeightSynchronous(position.GetX(), position.GetY(), sampler, terrainExistsPtr);
}
float TerrainSystem::GetHeightFromVector2(const AZ::Vector2& 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::GetIsHole(const AZ::Vector3& position, Sampler sampler) const
{
bool terrainExists = false;
GetHeightSynchronous(position.GetX(), position.GetY(), sampler, &terrainExists);
return !terrainExists;
}
bool TerrainSystem::GetIsHoleFromVector2(const AZ::Vector2& position, Sampler sampler) const
{
bool terrainExists = false;
GetHeightSynchronous(position.GetX(), position.GetY(), sampler, &terrainExists);
return !terrainExists;
}
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
{
AZStd::shared_lock<AZStd::shared_mutex> lock(m_areaMutex);
bool terrainExists = false;
AZ::Vector3 outNormal = AZ::Vector3::CreateAxisZ();
if (!InWorldBounds(x, y))
{
if (terrainExistsPtr)
{
*terrainExistsPtr = terrainExists;
return outNormal;
}
}
const AZ::Vector2 range = (m_currentSettings.m_heightQueryResolution / 2.0f);
const AZ::Vector2 left (x - range.GetX(), y);
const AZ::Vector2 right(x + range.GetX(), y);
const AZ::Vector2 up (x, y - range.GetY());
const AZ::Vector2 down (x, y + range.GetY());
AZ::Vector3 v1(up.GetX(), up.GetY(), GetHeightSynchronous(up.GetX(), up.GetY(), sampler, &terrainExists));
AZ::Vector3 v2(left.GetX(), left.GetY(), GetHeightSynchronous(left.GetX(), left.GetY(), sampler, &terrainExists));
AZ::Vector3 v3(right.GetX(), right.GetY(), GetHeightSynchronous(right.GetX(), right.GetY(), sampler, &terrainExists));
AZ::Vector3 v4(down.GetX(), down.GetY(), GetHeightSynchronous(down.GetX(), down.GetY(), sampler, &terrainExists));
outNormal = (v3 - v2).Cross(v4 - v1).GetNormalized();
if (terrainExistsPtr)
{
*terrainExistsPtr = terrainExists;
}
return outNormal;
}
AZ::Vector3 TerrainSystem::GetNormal(const AZ::Vector3& position, Sampler sampler, bool* terrainExistsPtr) const
{
return GetNormalSynchronous(position.GetX(), position.GetY(), sampler, terrainExistsPtr);
}
AZ::Vector3 TerrainSystem::GetNormalFromVector2(const AZ::Vector2& 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(
const AZ::Vector3& position, Sampler sampleFilter, bool* terrainExistsPtr) const
{
return GetMaxSurfaceWeightFromFloats(position.GetX(), position.GetY(), sampleFilter, terrainExistsPtr);
}
AzFramework::SurfaceData::SurfaceTagWeight TerrainSystem::GetMaxSurfaceWeightFromVector2(
const AZ::Vector2& inPosition, Sampler sampleFilter, bool* terrainExistsPtr) const
{
return GetMaxSurfaceWeightFromFloats(inPosition.GetX(), inPosition.GetY(), sampleFilter, terrainExistsPtr);
}
AzFramework::SurfaceData::SurfaceTagWeight TerrainSystem::GetMaxSurfaceWeightFromFloats(
const float x, const float y, Sampler sampleFilter, bool* terrainExistsPtr) const
{
if (terrainExistsPtr)
{
*terrainExistsPtr = true;
}
AzFramework::SurfaceData::SurfaceTagWeightList weightSet;
if (!InWorldBounds(x, y))
{
if (terrainExistsPtr)
{
*terrainExistsPtr = false;
return {};
}
}
GetOrderedSurfaceWeights(x, y, sampleFilter, weightSet, terrainExistsPtr);
if (weightSet.empty())
{
return {};
}
return *weightSet.begin();
}
void TerrainSystem::GetSurfacePoint(
const AZ::Vector3& inPosition,
AzFramework::SurfaceData::SurfacePoint& outSurfacePoint,
Sampler sampleFilter,
bool* terrainExistsPtr) const
{
outSurfacePoint.m_position = inPosition;
outSurfacePoint.m_position.SetZ(GetHeightSynchronous(inPosition.GetX(), inPosition.GetY(), sampleFilter, terrainExistsPtr));
outSurfacePoint.m_normal = GetNormalSynchronous(inPosition.GetX(), inPosition.GetY(), sampleFilter, nullptr);
GetSurfaceWeights(inPosition, outSurfacePoint.m_surfaceTags, sampleFilter, nullptr);
}
void TerrainSystem::GetSurfacePointFromVector2(
const AZ::Vector2& inPosition,
AzFramework::SurfaceData::SurfacePoint& outSurfacePoint,
Sampler sampleFilter,
bool* terrainExistsPtr) const
{
GetSurfacePoint(AZ::Vector3(inPosition.GetX(), inPosition.GetY(), 0.0f), outSurfacePoint, sampleFilter, terrainExistsPtr);
}
void TerrainSystem::GetSurfacePointFromFloats(
float x,
float y,
AzFramework::SurfaceData::SurfacePoint& outSurfacePoint,
Sampler sampleFilter,
bool* terrainExistsPtr) const
{
GetSurfacePoint(AZ::Vector3(x, y, 0.0f), outSurfacePoint, sampleFilter, terrainExistsPtr);
}
AzFramework::EntityContextId TerrainSystem::GetTerrainRaycastEntityContextId() const
{
return m_terrainRaycastContext.GetEntityContextId();
}
AzFramework::RenderGeometry::RayResult TerrainSystem::GetClosestIntersection(
const AzFramework::RenderGeometry::RayRequest& ray) const
{
return m_terrainRaycastContext.RayIntersect(ray);
}
AZ::EntityId TerrainSystem::FindBestAreaEntityAtPosition(float x, float y, AZ::Aabb& bounds) const
{
AZ::Vector3 inPosition = AZ::Vector3(x, y, 0);
// Find the highest priority layer that encompasses this position
AZStd::shared_lock<AZStd::shared_mutex> lock(m_areaMutex);
// The areas are sorted into priority order: the first area that contains inPosition is the most suitable.
for (const auto& [areaId, areaData] : m_registeredAreas)
{
inPosition.SetZ(areaData.m_areaBounds.GetMin().GetZ());
if (areaData.m_areaBounds.Contains(inPosition))
{
bounds = areaData.m_areaBounds;
return areaId;
}
}
return AZ::EntityId();
}
void TerrainSystem::GetOrderedSurfaceWeights(
const float x,
const float y,
[[maybe_unused]] Sampler sampler,
AzFramework::SurfaceData::SurfaceTagWeightList& outSurfaceWeights,
bool* terrainExistsPtr) const
{
AZ::Aabb bounds;
AZ::EntityId bestAreaId = FindBestAreaEntityAtPosition(x, y, bounds);
if (terrainExistsPtr)
{
GetHeightFromFloats(x, y, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, terrainExistsPtr);
}
outSurfaceWeights.clear();
if (!bestAreaId.IsValid())
{
return;
}
const AZ::Vector3 inPosition = AZ::Vector3(x, y, 0.0f);
// Get all the surfaces with weights at the given point.
Terrain::TerrainAreaSurfaceRequestBus::Event(
bestAreaId, &Terrain::TerrainAreaSurfaceRequestBus::Events::GetSurfaceWeights, inPosition, outSurfaceWeights);
AZStd::sort(outSurfaceWeights.begin(), outSurfaceWeights.end(), AzFramework::SurfaceData::SurfaceTagWeightComparator());
}
void TerrainSystem::GetSurfaceWeights(
const AZ::Vector3& inPosition,
AzFramework::SurfaceData::SurfaceTagWeightList& outSurfaceWeights,
Sampler sampleFilter,
bool* terrainExistsPtr) const
{
GetOrderedSurfaceWeights(inPosition.GetX(), inPosition.GetY(), sampleFilter, outSurfaceWeights, terrainExistsPtr);
}
void TerrainSystem::GetSurfaceWeightsFromVector2(
const AZ::Vector2& inPosition,
AzFramework::SurfaceData::SurfaceTagWeightList& outSurfaceWeights,
Sampler sampleFilter,
bool* terrainExistsPtr) const
{
GetOrderedSurfaceWeights(inPosition.GetX(), inPosition.GetY(), sampleFilter, outSurfaceWeights, terrainExistsPtr);
}
void TerrainSystem::GetSurfaceWeightsFromFloats(
float x, float y,
AzFramework::SurfaceData::SurfaceTagWeightList& outSurfaceWeights,
Sampler sampleFilter,
bool* terrainExistsPtr) const
{
GetOrderedSurfaceWeights(x, y, sampleFilter, outSurfaceWeights, terrainExistsPtr);
}
const char* TerrainSystem::GetMaxSurfaceName(
[[maybe_unused]] const AZ::Vector3& position, [[maybe_unused]] Sampler sampleFilter, [[maybe_unused]] bool* terrainExistsPtr) const
{
// For now, always set terrainExists to true, as we don't have a way to author data for terrain holes yet.
if (terrainExistsPtr)
{
*terrainExistsPtr = true;
}
return "";
}
void TerrainSystem::ProcessHeightsFromList(
const AZStd::span<AZ::Vector3>& inPositions,
AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
Sampler sampleFilter) const
{
if (!perPositionCallback)
{
return;
}
AzFramework::SurfaceData::SurfacePoint surfacePoint;
for (const auto& position : inPositions)
{
bool terrainExists = false;
surfacePoint.m_position = position;
surfacePoint.m_position.SetZ(GetHeight(position, sampleFilter, &terrainExists));
perPositionCallback(surfacePoint, terrainExists);
}
}
void TerrainSystem::ProcessNormalsFromList(
const AZStd::span<AZ::Vector3>& inPositions,
AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
Sampler sampleFilter) const
{
if (!perPositionCallback)
{
return;
}
AzFramework::SurfaceData::SurfacePoint surfacePoint;
for (const auto& position : inPositions)
{
bool terrainExists = false;
surfacePoint.m_position = position;
surfacePoint.m_normal = GetNormal(position, sampleFilter, &terrainExists);
perPositionCallback(surfacePoint, terrainExists);
}
}
void TerrainSystem::ProcessSurfaceWeightsFromList(
const AZStd::span<AZ::Vector3>& inPositions,
AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
Sampler sampleFilter) const
{
if (!perPositionCallback)
{
return;
}
AzFramework::SurfaceData::SurfacePoint surfacePoint;
for (const auto& position : inPositions)
{
bool terrainExists = false;
surfacePoint.m_position = position;
GetSurfaceWeights(position, surfacePoint.m_surfaceTags, sampleFilter, &terrainExists);
perPositionCallback(surfacePoint, terrainExists);
}
}
void TerrainSystem::ProcessSurfacePointsFromList(
const AZStd::span<AZ::Vector3>& inPositions,
AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
Sampler sampleFilter) const
{
if (!perPositionCallback)
{
return;
}
AzFramework::SurfaceData::SurfacePoint surfacePoint;
for (const auto& position : inPositions)
{
bool terrainExists = false;
surfacePoint.m_position = position;
GetSurfacePoint(position, surfacePoint, sampleFilter, &terrainExists);
perPositionCallback(surfacePoint, terrainExists);
}
}
void TerrainSystem::ProcessHeightsFromListOfVector2(
const AZStd::span<AZ::Vector2>& inPositions,
AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
Sampler sampleFilter) const
{
if (!perPositionCallback)
{
return;
}
AzFramework::SurfaceData::SurfacePoint surfacePoint;
for (const auto& position : inPositions)
{
bool terrainExists = false;
surfacePoint.m_position.Set(position.GetX(), position.GetY(), 0.0f);
surfacePoint.m_position.SetZ(GetHeightFromVector2(position, sampleFilter, &terrainExists));
perPositionCallback(surfacePoint, terrainExists);
}
}
void TerrainSystem::ProcessNormalsFromListOfVector2(
const AZStd::span<AZ::Vector2>& inPositions,
AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
Sampler sampleFilter) const
{
if (!perPositionCallback)
{
return;
}
AzFramework::SurfaceData::SurfacePoint surfacePoint;
for (const auto& position : inPositions)
{
bool terrainExists = false;
surfacePoint.m_position.Set(position.GetX(), position.GetY(), 0.0f);
surfacePoint.m_normal = GetNormalFromVector2(position, sampleFilter, &terrainExists);
perPositionCallback(surfacePoint, terrainExists);
}
}
void TerrainSystem::ProcessSurfaceWeightsFromListOfVector2(
const AZStd::span<AZ::Vector2>& inPositions,
AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
Sampler sampleFilter) const
{
if (!perPositionCallback)
{
return;
}
AzFramework::SurfaceData::SurfacePoint surfacePoint;
for (const auto& position : inPositions)
{
bool terrainExists = false;
surfacePoint.m_position.Set(position.GetX(), position.GetY(), 0.0f);
GetSurfaceWeightsFromVector2(position, surfacePoint.m_surfaceTags, sampleFilter, &terrainExists);
perPositionCallback(surfacePoint, terrainExists);
}
}
void TerrainSystem::ProcessSurfacePointsFromListOfVector2(
const AZStd::span<AZ::Vector2>& inPositions,
AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback,
Sampler sampleFilter) const
{
if (!perPositionCallback)
{
return;
}
AzFramework::SurfaceData::SurfacePoint surfacePoint;
for (const auto& position : inPositions)
{
bool terrainExists = false;
surfacePoint.m_position.Set(position.GetX(), position.GetY(), 0.0f);
GetSurfacePointFromVector2(position, surfacePoint, sampleFilter, &terrainExists);
perPositionCallback(surfacePoint, terrainExists);
}
}
AZStd::pair<size_t, size_t> TerrainSystem::GetNumSamplesFromRegion(
const AZ::Aabb& inRegion,
const AZ::Vector2& stepSize) const
{
const size_t numSamplesX = aznumeric_cast<size_t>(ceil(inRegion.GetExtents().GetX() / stepSize.GetX()));
const size_t numSamplesY = aznumeric_cast<size_t>(ceil(inRegion.GetExtents().GetY() / stepSize.GetY()));
return AZStd::make_pair(numSamplesX, numSamplesY);
}
void TerrainSystem::ProcessHeightsFromRegion(
const AZ::Aabb& inRegion,
const AZ::Vector2& stepSize,
AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
Sampler sampleFilter) const
{
// Don't bother processing if we don't have a callback
if (!perPositionCallback)
{
return;
}
const size_t numSamplesX = aznumeric_cast<size_t>(ceil(inRegion.GetExtents().GetX() / stepSize.GetX()));
const size_t numSamplesY = aznumeric_cast<size_t>(ceil(inRegion.GetExtents().GetY() / stepSize.GetY()));
AzFramework::SurfaceData::SurfacePoint surfacePoint;
for (size_t y = 0; y < numSamplesY; y++)
{
float fy = aznumeric_cast<float>(inRegion.GetMin().GetY() + (y * stepSize.GetY()));
for (size_t x = 0; x < numSamplesX; x++)
{
bool terrainExists = false;
float fx = aznumeric_cast<float>(inRegion.GetMin().GetX() + (x * stepSize.GetX()));
surfacePoint.m_position.Set(fx, fy, 0.0f);
surfacePoint.m_position.SetZ(GetHeight(surfacePoint.m_position, sampleFilter, &terrainExists));
perPositionCallback(x, y, surfacePoint, terrainExists);
}
}
}
void TerrainSystem::ProcessNormalsFromRegion(
const AZ::Aabb& inRegion,
const AZ::Vector2& stepSize,
AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
Sampler sampleFilter) const
{
// Don't bother processing if we don't have a callback
if (!perPositionCallback)
{
return;
}
const size_t numSamplesX = aznumeric_cast<size_t>(ceil(inRegion.GetExtents().GetX() / stepSize.GetX()));
const size_t numSamplesY = aznumeric_cast<size_t>(ceil(inRegion.GetExtents().GetY() / stepSize.GetY()));
AzFramework::SurfaceData::SurfacePoint surfacePoint;
for (size_t y = 0; y < numSamplesY; y++)
{
float fy = aznumeric_cast<float>(inRegion.GetMin().GetY() + (y * stepSize.GetY()));
for (size_t x = 0; x < numSamplesX; x++)
{
bool terrainExists = false;
float fx = aznumeric_cast<float>(inRegion.GetMin().GetX() + (x * stepSize.GetX()));
surfacePoint.m_position.Set(fx, fy, 0.0f);
surfacePoint.m_normal = GetNormal(surfacePoint.m_position, sampleFilter, &terrainExists);
perPositionCallback(x, y, surfacePoint, terrainExists);
}
}
}
void TerrainSystem::ProcessSurfaceWeightsFromRegion(
const AZ::Aabb& inRegion,
const AZ::Vector2& stepSize,
AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
Sampler sampleFilter) const
{
// Don't bother processing if we don't have a callback
if (!perPositionCallback)
{
return;
}
const size_t numSamplesX = aznumeric_cast<size_t>(ceil(inRegion.GetExtents().GetX() / stepSize.GetX()));
const size_t numSamplesY = aznumeric_cast<size_t>(ceil(inRegion.GetExtents().GetY() / stepSize.GetY()));
AzFramework::SurfaceData::SurfacePoint surfacePoint;
for (size_t y = 0; y < numSamplesY; y++)
{
float fy = aznumeric_cast<float>(inRegion.GetMin().GetY() + (y * stepSize.GetY()));
for (size_t x = 0; x < numSamplesX; x++)
{
bool terrainExists = false;
float fx = aznumeric_cast<float>(inRegion.GetMin().GetX() + (x * stepSize.GetX()));
surfacePoint.m_position.Set(fx, fy, 0.0f);
GetSurfaceWeights(surfacePoint.m_position, surfacePoint.m_surfaceTags, sampleFilter, &terrainExists);
perPositionCallback(x, y, surfacePoint, terrainExists);
}
}
}
void TerrainSystem::ProcessSurfacePointsFromRegion(
const AZ::Aabb& inRegion,
const AZ::Vector2& stepSize,
AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
Sampler sampleFilter) const
{
// Don't bother processing if we don't have a callback
if (!perPositionCallback)
{
return;
}
const size_t numSamplesX = aznumeric_cast<size_t>(ceil(inRegion.GetExtents().GetX() / stepSize.GetX()));
const size_t numSamplesY = aznumeric_cast<size_t>(ceil(inRegion.GetExtents().GetY() / stepSize.GetY()));
AzFramework::SurfaceData::SurfacePoint surfacePoint;
for (size_t y = 0; y < numSamplesY; y++)
{
float fy = aznumeric_cast<float>(inRegion.GetMin().GetY() + (y * stepSize.GetY()));
for (size_t x = 0; x < numSamplesX; x++)
{
bool terrainExists = false;
float fx = aznumeric_cast<float>(inRegion.GetMin().GetX() + (x * stepSize.GetX()));
surfacePoint.m_position.Set(fx, fy, 0.0f);
GetSurfacePoint(surfacePoint.m_position, surfacePoint, sampleFilter, &terrainExists);
perPositionCallback(x, y, surfacePoint, terrainExists);
}
}
}
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);
// Cache off whether or not this layer spawner should have a default ground plane when no other terrain height data exists.
bool useGroundPlane = false;
Terrain::TerrainSpawnerRequestBus::EventResult(useGroundPlane, areaId, &Terrain::TerrainSpawnerRequestBus::Events::GetUseGroundPlane);
m_registeredAreas[areaId] = { aabb, useGroundPlane };
m_dirtyRegion.AddAabb(aabb);
m_terrainHeightDirty = true;
m_terrainSurfacesDirty = 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, areaData] = item;
if (areaId == entityId)
{
m_dirtyRegion.AddAabb(areaData.m_areaBounds);
m_terrainHeightDirty = true;
m_terrainSurfacesDirty = true;
return true;
}
return false;
});
}
void TerrainSystem::RefreshArea(AZ::EntityId areaId, AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask changeMask)
{
using Terrain = AzFramework::Terrain::TerrainDataNotifications;
AZStd::unique_lock<AZStd::shared_mutex> lock(m_areaMutex);
auto areaAabb = m_registeredAreas.find(areaId);
AZ::Aabb oldAabb = (areaAabb != m_registeredAreas.end()) ? areaAabb->second.m_areaBounds : AZ::Aabb::CreateNull();
AZ::Aabb newAabb = AZ::Aabb::CreateNull();
LmbrCentral::ShapeComponentRequestsBus::EventResult(newAabb, areaId, &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb);
m_registeredAreas[areaId].m_areaBounds = newAabb;
AZ::Aabb expandedAabb = oldAabb;
expandedAabb.AddAabb(newAabb);
m_dirtyRegion.AddAabb(expandedAabb);
// Keep track of which types of data have changed so that we can send out the appropriate notifications later.
m_terrainHeightDirty = m_terrainHeightDirty || ((changeMask & Terrain::HeightData) == Terrain::HeightData);
m_terrainSurfacesDirty = m_terrainSurfacesDirty || ((changeMask & Terrain::SurfaceData) == Terrain::SurfaceData);
}
void TerrainSystem::OnTick(float /*deltaTime*/, AZ::ScriptTimePoint /*time*/)
{
using Terrain = AzFramework::Terrain::TerrainDataNotifications;
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_terrainSurfacesDirty = 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_terrainSurfacesDirty = true;
}
m_currentSettings = m_requestedSettings;
}
if (terrainSettingsChanged || m_terrainHeightDirty || m_terrainSurfacesDirty)
{
// 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);
Terrain::TerrainDataChangedMask changeMask = Terrain::TerrainDataChangedMask::None;
if (terrainSettingsChanged)
{
changeMask = static_cast<Terrain::TerrainDataChangedMask>(changeMask | Terrain::TerrainDataChangedMask::Settings);
}
if (m_terrainHeightDirty)
{
changeMask = static_cast<Terrain::TerrainDataChangedMask>(changeMask | Terrain::TerrainDataChangedMask::HeightData);
}
if (m_terrainSurfacesDirty)
{
changeMask = static_cast<Terrain::TerrainDataChangedMask>(changeMask | Terrain::TerrainDataChangedMask::SurfaceData);
}
// 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_terrainSurfacesDirty = false;
m_dirtyRegion = AZ::Aabb::CreateNull();
AzFramework::Terrain::TerrainDataNotificationBus::Broadcast(
&AzFramework::Terrain::TerrainDataNotificationBus::Events::OnTerrainDataChanged, dirtyRegion,
changeMask);
}
}