/* * 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 #include #include #include #include #include #include #include #include #include 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)); // Use the global JobManager for terrain jobs (we could create our own dedicated terrain JobManager if needed). AZ::JobManagerBus::BroadcastResult(m_terrainJobManager, &AZ::JobManagerEvents::GetManager); AZ_Assert(m_terrainJobManager, "No global JobManager found."); } 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 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() { { // Cancel all active terrain jobs, and wait until they have completed. AZStd::unique_lock lock(m_activeTerrainJobContextMutex); for (auto activeTerrainJobContext : m_activeTerrainJobContexts) { activeTerrainJobContext->Cancel(); } m_activeTerrainJobContextMutexConditionVariable.wait(lock, [this]{ return m_activeTerrainJobContexts.empty(); }); } // 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 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(float queryResolution) { m_requestedSettings.m_heightQueryResolution = queryResolution; m_terrainSettingsDirty = true; } AZ::Aabb TerrainSystem::GetTerrainAabb() const { return m_currentSettings.m_worldBounds; } float 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; } // Generate positions to be queried based on the sampler type. void TerrainSystem::GenerateQueryPositions(const AZStd::span& inPositions, AZStd::vector& outPositions, Sampler sampler) const { const float minHeight = m_currentSettings.m_worldBounds.GetMin().GetZ(); for (auto& position : inPositions) { switch(sampler) { case AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR: { AZ::Vector2 normalizedDelta; AZ::Vector2 pos0; ClampPosition(position.GetX(), position.GetY(), pos0, normalizedDelta); const AZ::Vector2 pos1(pos0.GetX() + m_currentSettings.m_heightQueryResolution, pos0.GetY() + m_currentSettings.m_heightQueryResolution); outPositions.emplace_back(AZ::Vector3(pos0.GetX(), pos0.GetY(), minHeight)); outPositions.emplace_back(AZ::Vector3(pos1.GetX(), pos0.GetY(), minHeight)); outPositions.emplace_back(AZ::Vector3(pos0.GetX(), pos1.GetY(), minHeight)); outPositions.emplace_back(AZ::Vector3(pos1.GetX(), pos1.GetY(), minHeight)); } break; case AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP: { AZ::Vector2 normalizedDelta; AZ::Vector2 clampedPosition; ClampPosition(position.GetX(), position.GetY(), clampedPosition, normalizedDelta); outPositions.emplace_back(AZ::Vector3(clampedPosition.GetX(), clampedPosition.GetY(), minHeight)); } break; case AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT: [[fallthrough]]; default: outPositions.emplace_back(AZ::Vector3(position.GetX(), position.GetY(), minHeight)); break; } } } AZStd::vector TerrainSystem::GenerateInputPositionsFromRegion( const AZ::Aabb& inRegion, const AZ::Vector2& stepSize) const { AZStd::vector inPositions; const auto [numSamplesX, numSamplesY] = GetNumSamplesFromRegion(inRegion, stepSize); inPositions.reserve(numSamplesX * numSamplesY); for (size_t y = 0; y < numSamplesY; y++) { float fy = aznumeric_cast(inRegion.GetMin().GetY() + (y * stepSize.GetY())); for (size_t x = 0; x < numSamplesX; x++) { float fx = aznumeric_cast(inRegion.GetMin().GetX() + (x * stepSize.GetX())); inPositions.emplace_back(AZ::Vector3(fx, fy, 0.0f)); } } return inPositions; } AZStd::vector TerrainSystem::GenerateInputPositionsFromListOfVector2( const AZStd::span inPositionsVec2) const { AZStd::vector inPositions; inPositions.reserve(inPositionsVec2.size()); for (auto& pos : inPositionsVec2) { inPositions.emplace_back(AZ::Vector3(pos.GetX(), pos.GetY(), 0.0f)); } return inPositions; } void TerrainSystem::MakeBulkQueries( const AZStd::span inPositions, AZStd::span outPositions, AZStd::span outTerrainExists, AZStd::span outSurfaceWeights, BulkQueriesCallback queryCallback) const { AZ::Aabb bounds; AZ::EntityId prevAreaId = FindBestAreaEntityAtPosition(inPositions[0].GetX(), inPositions[0].GetY(), bounds); // We use a sliding window here and update the window end for each // position that falls in the same area as the previous positions. This consumes lesser memory // than sorting the points into separate lists and handling putting them back together. // This may be sub optimal if the points are randomly distributed in the list as opposed // to points in the same area id being close to each other. size_t windowStart = 0; size_t windowEnd = 0; const size_t numPositions = inPositions.size(); for(int i = 1; i < numPositions; i++) { AZ::EntityId areaId = FindBestAreaEntityAtPosition(inPositions[i].GetX(), inPositions[i].GetY(), bounds); bool queryHeights = false; if (areaId == prevAreaId) { // Update window end to current position. // If it's the last position, submit the query. windowEnd = i; if (windowEnd == numPositions - 1) { queryHeights = true; } } else { queryHeights = true; } if (queryHeights) { // If the area id is a default entity id, it usually means the // position is outside world bounds. if (prevAreaId != AZ::EntityId()) { size_t spanLength = (windowEnd - windowStart) + 1; queryCallback(AZStd::span(inPositions.begin() + windowStart, spanLength), AZStd::span(outPositions.begin() + windowStart, spanLength), AZStd::span(outTerrainExists.begin() + windowStart, spanLength), AZStd::span(outSurfaceWeights.begin() + windowStart, spanLength), prevAreaId); } // Reset the window to start at the current position. Set the new area // id on which to run the next query. windowStart = windowEnd = i; prevAreaId = areaId; } } } void TerrainSystem::GetHeightsSynchronous(const AZStd::span& inPositions, Sampler sampler, AZStd::span heights, AZStd::span terrainExists) const { AZStd::shared_lock lock(m_areaMutex); AZStd::vector outPositions; AZStd::vector outTerrainExists; // outPositions holds the iterators to results of the bulk queries. // In the case of the bilinear sampler, we'll be making 4 queries per // input position. size_t indexStepSize = (sampler == AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) ? 4 : 1; outPositions.reserve(inPositions.size() * indexStepSize); outTerrainExists.resize(inPositions.size() * indexStepSize); GenerateQueryPositions(inPositions, outPositions, sampler); auto callback = []([[maybe_unused]] const AZStd::span inPositions, AZStd::span outPositions, AZStd::span outTerrainExists, [[maybe_unused]] AZStd::span outSurfaceWeights, AZ::EntityId areaId) { AZ_Assert((inPositions.size() == outPositions.size() && inPositions.size() == outTerrainExists.size()), "The sizes of the terrain exists list and in/out positions list should match."); Terrain::TerrainAreaHeightRequestBus::Event(areaId, &Terrain::TerrainAreaHeightRequestBus::Events::GetHeights, outPositions, outTerrainExists); }; // This will be unused for heights. It's fine if it's empty. AZStd::vector outSurfaceWeights; MakeBulkQueries(outPositions, outPositions, outTerrainExists, outSurfaceWeights, callback); // Compute/store the final result for (size_t i = 0, iteratorIndex = 0; i < inPositions.size(); i++, iteratorIndex += indexStepSize) { switch(sampler) { case AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR: { // We now need to compute the final height after all the bulk queries are done. AZ::Vector2 normalizedDelta; AZ::Vector2 clampedPosition; ClampPosition(inPositions[i].GetX(), inPositions[i].GetY(), clampedPosition, normalizedDelta); const float heightX0Y0 = outPositions[iteratorIndex].GetZ(); const float heightX1Y0 = outPositions[iteratorIndex + 1].GetZ(); const float heightX0Y1 = outPositions[iteratorIndex + 2].GetZ(); const float heightX1Y1 = outPositions[iteratorIndex + 3].GetZ(); const float heightXY0 = AZ::Lerp(heightX0Y0, heightX1Y0, normalizedDelta.GetX()); const float heightXY1 = AZ::Lerp(heightX0Y1, heightX1Y1, normalizedDelta.GetX()); heights[i] = AZ::Lerp(heightXY0, heightXY1, normalizedDelta.GetY()); terrainExists[i] = outTerrainExists[iteratorIndex]; } break; case AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP: [[fallthrough]]; case AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT: [[fallthrough]]; default: // For clamp and exact, we just need to store the results of the bulk query. heights[i] = outPositions[iteratorIndex].GetZ(); terrainExists[i] = outTerrainExists[iteratorIndex]; break; } } } 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 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 + AZ::Vector2(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 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; } void TerrainSystem::GetNormalsSynchronous(const AZStd::span& inPositions, Sampler sampler, AZStd::span normals, AZStd::span terrainExists) const { AZStd::vector directionVectors; directionVectors.reserve(inPositions.size() * 4); const AZ::Vector2 range(m_currentSettings.m_heightQueryResolution / 2.0f, m_currentSettings.m_heightQueryResolution / 2.0f); size_t indexStepSize = 4; for (auto& position : inPositions) { directionVectors.emplace_back(position.GetX(), position.GetY() - range.GetY(), 0.0f); directionVectors.emplace_back(position.GetX() - range.GetX(), position.GetY(), 0.0f); directionVectors.emplace_back(position.GetX() + range.GetX(), position.GetY(), 0.0f); directionVectors.emplace_back(position.GetX(), position.GetY() + range.GetY(), 0.0f); } AZStd::vector heights(directionVectors.size()); AZStd::vector exists(directionVectors.size()); GetHeightsSynchronous(directionVectors, sampler, heights, exists); for (size_t i = 0, iteratorIndex = 0; i < inPositions.size(); i++, iteratorIndex += indexStepSize) { directionVectors[iteratorIndex].SetZ(heights[iteratorIndex]); directionVectors[iteratorIndex + 1].SetZ(heights[iteratorIndex + 1]); directionVectors[iteratorIndex + 2].SetZ(heights[iteratorIndex + 2]); directionVectors[iteratorIndex + 3].SetZ(heights[iteratorIndex + 3]); normals[i] = (directionVectors[iteratorIndex + 2] - directionVectors[iteratorIndex + 1]). Cross(directionVectors[iteratorIndex + 3] - directionVectors[iteratorIndex]).GetNormalized(); terrainExists[i] = exists[iteratorIndex]; } } AZ::Vector3 TerrainSystem::GetNormalSynchronous(float x, float y, Sampler sampler, bool* terrainExistsPtr) const { AZStd::shared_lock lock(m_areaMutex); bool terrainExists = false; AZ::Vector3 outNormal = AZ::Vector3::CreateAxisZ(); if (!InWorldBounds(x, y)) { if (terrainExistsPtr) { *terrainExistsPtr = terrainExists; return outNormal; } } float range = m_currentSettings.m_heightQueryResolution / 2.0f; const AZ::Vector2 left (x - range, y); const AZ::Vector2 right(x + range, y); const AZ::Vector2 up (x, y - range); const AZ::Vector2 down (x, y + range); 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); } AZStd::shared_ptr TerrainSystem::ProcessHeightsFromListAsync( const AZStd::span& inPositions, AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, Sampler sampleFilter, AZStd::shared_ptr params) const { return ProcessFromListAsync(AZStd::bind(&TerrainSystem::ProcessHeightsFromList, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3), inPositions, perPositionCallback, sampleFilter, params); } AZStd::shared_ptr TerrainSystem::ProcessNormalsFromListAsync( const AZStd::span& inPositions, AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, Sampler sampleFilter, AZStd::shared_ptr params) const { return ProcessFromListAsync(AZStd::bind(&TerrainSystem::ProcessNormalsFromList, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3), inPositions, perPositionCallback, sampleFilter, params); } AZStd::shared_ptr TerrainSystem::ProcessSurfaceWeightsFromListAsync( const AZStd::span& inPositions, AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, Sampler sampleFilter, AZStd::shared_ptr params) const { return ProcessFromListAsync(AZStd::bind(&TerrainSystem::ProcessSurfaceWeightsFromList, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3), inPositions, perPositionCallback, sampleFilter, params); } AZStd::shared_ptr TerrainSystem::ProcessSurfacePointsFromListAsync( const AZStd::span& inPositions, AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, Sampler sampleFilter, AZStd::shared_ptr params) const { return ProcessFromListAsync(AZStd::bind(&TerrainSystem::ProcessSurfacePointsFromList, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3), inPositions, perPositionCallback, sampleFilter, params); } AZStd::shared_ptr TerrainSystem::ProcessHeightsFromListOfVector2Async( const AZStd::span& inPositions, AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, Sampler sampleFilter, AZStd::shared_ptr params) const { return ProcessFromListAsync(AZStd::bind(&TerrainSystem::ProcessHeightsFromListOfVector2, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3), inPositions, perPositionCallback, sampleFilter, params); } AZStd::shared_ptr TerrainSystem::ProcessNormalsFromListOfVector2Async( const AZStd::span& inPositions, AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, Sampler sampleFilter, AZStd::shared_ptr params) const { return ProcessFromListAsync(AZStd::bind(&TerrainSystem::ProcessNormalsFromListOfVector2, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3), inPositions, perPositionCallback, sampleFilter, params); } AZStd::shared_ptr TerrainSystem::ProcessSurfaceWeightsFromListOfVector2Async( const AZStd::span& inPositions, AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, Sampler sampleFilter, AZStd::shared_ptr params) const { return ProcessFromListAsync(AZStd::bind(&TerrainSystem::ProcessSurfaceWeightsFromListOfVector2, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3), inPositions, perPositionCallback, sampleFilter, params); } AZStd::shared_ptr TerrainSystem::ProcessSurfacePointsFromListOfVector2Async( const AZStd::span& inPositions, AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, Sampler sampleFilter, AZStd::shared_ptr params) const { return ProcessFromListAsync(AZStd::bind(&TerrainSystem::ProcessSurfacePointsFromListOfVector2, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3), inPositions, perPositionCallback, sampleFilter, params); } AZStd::shared_ptr TerrainSystem::ProcessHeightsFromRegionAsync( const AZ::Aabb& inRegion, const AZ::Vector2& stepSize, AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback, Sampler sampleFilter, AZStd::shared_ptr params) const { return ProcessFromRegionAsync(AZStd::bind(&TerrainSystem::ProcessHeightsFromRegion, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), inRegion, stepSize, perPositionCallback, sampleFilter, params); } AZStd::shared_ptr TerrainSystem::ProcessNormalsFromRegionAsync( const AZ::Aabb& inRegion, const AZ::Vector2& stepSize, AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback, Sampler sampleFilter, AZStd::shared_ptr params) const { return ProcessFromRegionAsync(AZStd::bind(&TerrainSystem::ProcessNormalsFromRegion, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), inRegion, stepSize, perPositionCallback, sampleFilter, params); } AZStd::shared_ptr TerrainSystem::ProcessSurfaceWeightsFromRegionAsync( const AZ::Aabb& inRegion, const AZ::Vector2& stepSize, AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback, Sampler sampleFilter, AZStd::shared_ptr params) const { return ProcessFromRegionAsync(AZStd::bind(&TerrainSystem::ProcessSurfaceWeightsFromRegion, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), inRegion, stepSize, perPositionCallback, sampleFilter, params); } AZStd::shared_ptr TerrainSystem::ProcessSurfacePointsFromRegionAsync( const AZ::Aabb& inRegion, const AZ::Vector2& stepSize, AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback, Sampler sampleFilter, AZStd::shared_ptr params) const { return ProcessFromRegionAsync(AZStd::bind(&TerrainSystem::ProcessSurfacePointsFromRegion, this, AZStd::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), inRegion, stepSize, perPositionCallback, sampleFilter, params); } 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 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::GetOrderedSurfaceWeightsFromList( const AZStd::span& inPositions, [[maybe_unused]] Sampler sampler, AZStd::span outSurfaceWeightsList, AZStd::span terrainExists) const { if (terrainExists.size() == outSurfaceWeightsList.size()) { AZStd::vector heights(inPositions.size()); GetHeightsSynchronous(inPositions, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, heights, terrainExists); } auto callback = [](const AZStd::span inPositions, [[maybe_unused]] AZStd::span outPositions, [[maybe_unused]] AZStd::span outTerrainExists, AZStd::span outSurfaceWeights, AZ::EntityId areaId) { AZ_Assert(inPositions.size() == outSurfaceWeights.size(), "The sizes of the surface weights list and in/out positions list should match."); Terrain::TerrainAreaSurfaceRequestBus::Event(areaId, &Terrain::TerrainAreaSurfaceRequestBus::Events::GetSurfaceWeightsFromList, inPositions, outSurfaceWeights); }; // This will be unused for surface weights. It's fine if it's empty. AZStd::vector outPositions; MakeBulkQueries(inPositions, outPositions, terrainExists, outSurfaceWeightsList, callback); } 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& inPositions, AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, Sampler sampleFilter) const { if (!perPositionCallback) { return; } AZStd::vector terrainExists(inPositions.size()); AZStd::vector heights(inPositions.size()); GetHeightsSynchronous(inPositions, sampleFilter, heights, terrainExists); AzFramework::SurfaceData::SurfacePoint surfacePoint; for (size_t i = 0; i < inPositions.size(); i++) { surfacePoint.m_position = inPositions[i]; surfacePoint.m_position.SetZ(heights[i]); perPositionCallback(surfacePoint, terrainExists[i]); } } void TerrainSystem::ProcessNormalsFromList( const AZStd::span& inPositions, AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, Sampler sampleFilter) const { if (!perPositionCallback) { return; } AZStd::vector terrainExists(inPositions.size()); AZStd::vector normals(inPositions.size()); GetNormalsSynchronous(inPositions, sampleFilter, normals, terrainExists); AzFramework::SurfaceData::SurfacePoint surfacePoint; for (size_t i = 0; i < inPositions.size(); i++) { surfacePoint.m_position = inPositions[i]; surfacePoint.m_normal = AZStd::move(normals[i]); perPositionCallback(surfacePoint, terrainExists[i]); } } void TerrainSystem::ProcessSurfaceWeightsFromList( const AZStd::span& inPositions, AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, Sampler sampleFilter) const { if (!perPositionCallback) { return; } AZStd::vector outSurfaceWeightsList(inPositions.size()); AZStd::vector terrainExists(inPositions.size()); GetOrderedSurfaceWeightsFromList(inPositions, sampleFilter, outSurfaceWeightsList, terrainExists); AzFramework::SurfaceData::SurfacePoint surfacePoint; for (size_t i = 0; i < inPositions.size(); i++) { surfacePoint.m_position = inPositions[i]; surfacePoint.m_surfaceTags = AZStd::move(outSurfaceWeightsList[i]); perPositionCallback(surfacePoint, terrainExists[i]); } } void TerrainSystem::ProcessSurfacePointsFromList( const AZStd::span& inPositions, AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, Sampler sampleFilter) const { if (!perPositionCallback) { return; } AZStd::vector heights(inPositions.size()); AZStd::vector normals(inPositions.size()); AZStd::vector outSurfaceWeightsList(inPositions.size()); AZStd::vector terrainExists(inPositions.size()); GetHeightsSynchronous(inPositions, sampleFilter, heights, terrainExists); GetNormalsSynchronous(inPositions, sampleFilter, normals, terrainExists); // We can skip the unnecessary call to GetHeights since we already // got the terrain exists flags in the earlier call to GetHeights AZStd::vector terrainExistsEmpty; GetOrderedSurfaceWeightsFromList(inPositions, sampleFilter, outSurfaceWeightsList, terrainExistsEmpty); AzFramework::SurfaceData::SurfacePoint surfacePoint; for (size_t i = 0; i < inPositions.size(); i++) { surfacePoint.m_position.Set(inPositions[i].GetX(), inPositions[i].GetY(), heights[i]); surfacePoint.m_normal = AZStd::move(normals[i]); surfacePoint.m_surfaceTags = AZStd::move(outSurfaceWeightsList[i]); perPositionCallback(surfacePoint, terrainExists[i]); } } void TerrainSystem::ProcessHeightsFromListOfVector2( const AZStd::span& inPositions, AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, Sampler sampleFilter) const { if (!perPositionCallback) { return; } AZStd::vector inPositionsVec3 = GenerateInputPositionsFromListOfVector2(inPositions); ProcessHeightsFromList(inPositionsVec3, perPositionCallback, sampleFilter); } void TerrainSystem::ProcessNormalsFromListOfVector2( const AZStd::span& inPositions, AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, Sampler sampleFilter) const { if (!perPositionCallback) { return; } AZStd::vector inPositionsVec3 = GenerateInputPositionsFromListOfVector2(inPositions); ProcessNormalsFromList(inPositionsVec3, perPositionCallback, sampleFilter); } void TerrainSystem::ProcessSurfaceWeightsFromListOfVector2( const AZStd::span& inPositions, AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, Sampler sampleFilter) const { if (!perPositionCallback) { return; } AZStd::vector inPositionsVec3 = GenerateInputPositionsFromListOfVector2(inPositions); ProcessSurfaceWeightsFromList(inPositionsVec3, perPositionCallback, sampleFilter); } void TerrainSystem::ProcessSurfacePointsFromListOfVector2( const AZStd::span& inPositions, AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, Sampler sampleFilter) const { if (!perPositionCallback) { return; } AZStd::vector inPositionsVec3 = GenerateInputPositionsFromListOfVector2(inPositions); ProcessSurfacePointsFromList(inPositionsVec3, perPositionCallback, sampleFilter); } AZStd::pair TerrainSystem::GetNumSamplesFromRegion( const AZ::Aabb& inRegion, const AZ::Vector2& stepSize) const { const size_t numSamplesX = aznumeric_cast(ceil(inRegion.GetExtents().GetX() / stepSize.GetX())); const size_t numSamplesY = aznumeric_cast(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 auto [numSamplesX, numSamplesY] = GetNumSamplesFromRegion(inRegion, stepSize); AZStd::vector inPositions = GenerateInputPositionsFromRegion(inRegion, stepSize); AZStd::vector terrainExists(inPositions.size()); AZStd::vector heights(inPositions.size()); GetHeightsSynchronous(inPositions, sampleFilter, heights, terrainExists); AzFramework::SurfaceData::SurfacePoint surfacePoint; for (size_t y = 0, i = 0; y < numSamplesY; y++) { for (size_t x = 0; x < numSamplesX; x++) { surfacePoint.m_position.Set(inPositions[i].GetX(), inPositions[i].GetY(), heights[i]); perPositionCallback(x, y, surfacePoint, terrainExists[i]); i++; } } } 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 auto [numSamplesX, numSamplesY] = GetNumSamplesFromRegion(inRegion, stepSize); AZStd::vector inPositions = GenerateInputPositionsFromRegion(inRegion, stepSize); AZStd::vector terrainExists(inPositions.size()); AZStd::vector normals(inPositions.size()); GetNormalsSynchronous(inPositions, sampleFilter, normals, terrainExists); AzFramework::SurfaceData::SurfacePoint surfacePoint; for (size_t y = 0, i = 0; y < numSamplesY; y++) { for (size_t x = 0; x < numSamplesX; x++) { surfacePoint.m_position.Set(inPositions[i].GetX(), inPositions[i].GetY(), 0.0f); surfacePoint.m_normal = AZStd::move(normals[i]); perPositionCallback(x, y, surfacePoint, terrainExists[i]); i++; } } } 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 auto [numSamplesX, numSamplesY] = GetNumSamplesFromRegion(inRegion, stepSize); AZStd::vector inPositions = GenerateInputPositionsFromRegion(inRegion, stepSize); AZStd::vector outSurfaceWeightsList(inPositions.size()); AZStd::vector terrainExists(inPositions.size()); GetOrderedSurfaceWeightsFromList(inPositions, sampleFilter, outSurfaceWeightsList, terrainExists); AzFramework::SurfaceData::SurfacePoint surfacePoint; for (size_t y = 0, i = 0; y < numSamplesY; y++) { for (size_t x = 0; x < numSamplesX; x++) { surfacePoint.m_position.Set(inPositions[i].GetX(), inPositions[i].GetY(), 0.0f); surfacePoint.m_surfaceTags = AZStd::move(outSurfaceWeightsList[i]); perPositionCallback(x, y, surfacePoint, terrainExists[i]); i++; } } } 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 auto [numSamplesX, numSamplesY] = GetNumSamplesFromRegion(inRegion, stepSize); AZStd::vector inPositions = GenerateInputPositionsFromRegion(inRegion, stepSize); AZStd::vector heights(inPositions.size()); AZStd::vector normals(inPositions.size()); AZStd::vector outSurfaceWeightsList(inPositions.size()); AZStd::vector terrainExists(inPositions.size()); GetHeightsSynchronous(inPositions, sampleFilter, heights, terrainExists); GetNormalsSynchronous(inPositions, sampleFilter, normals, terrainExists); // We can skip the unnecessary call to GetHeights since we already // got the terrain exists flags in the earlier call to GetHeights AZStd::vector terrainExistsEmpty; GetOrderedSurfaceWeightsFromList(inPositions, sampleFilter, outSurfaceWeightsList, terrainExistsEmpty); AzFramework::SurfaceData::SurfacePoint surfacePoint; for (size_t y = 0, i = 0; y < numSamplesY; y++) { for (size_t x = 0; x < numSamplesX; x++) { surfacePoint.m_position.Set(inPositions[i].GetX(), inPositions[i].GetY(), heights[i]); surfacePoint.m_normal = AZStd::move(normals[i]); surfacePoint.m_surfaceTags = AZStd::move(outSurfaceWeightsList[i]); perPositionCallback(x, y, surfacePoint, terrainExists[i]); i++; } } } void TerrainSystem::RegisterArea(AZ::EntityId areaId) { AZStd::unique_lock 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 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 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(changeMask | Terrain::TerrainDataChangedMask::Settings); } if (m_terrainHeightDirty) { changeMask = static_cast(changeMask | Terrain::TerrainDataChangedMask::HeightData); } if (m_terrainSurfacesDirty) { changeMask = static_cast(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); } }