diff --git a/Code/Framework/AzFramework/AzFramework/SurfaceData/SurfaceData.cpp b/Code/Framework/AzFramework/AzFramework/SurfaceData/SurfaceData.cpp index 55fec57bb6..e287c8d948 100644 --- a/Code/Framework/AzFramework/AzFramework/SurfaceData/SurfaceData.cpp +++ b/Code/Framework/AzFramework/AzFramework/SurfaceData/SurfaceData.cpp @@ -37,6 +37,7 @@ namespace AzFramework::SurfaceData { if (AZ::SerializeContext* serializeContext = azrtti_cast(context)) { + serializeContext->Class>(); serializeContext->Class() ->Field("m_position", &SurfacePoint::m_position) ->Field("m_normal", &SurfacePoint::m_normal) @@ -46,6 +47,7 @@ namespace AzFramework::SurfaceData if (AZ::BehaviorContext* behaviorContext = azrtti_cast(context)) { + behaviorContext->Class>(); behaviorContext->Class("AzFramework::SurfaceData::SurfacePoint") ->Attribute(AZ::Script::Attributes::Category, "SurfaceData") ->Constructor() diff --git a/Code/Framework/AzFramework/AzFramework/SurfaceData/SurfaceData.h b/Code/Framework/AzFramework/AzFramework/SurfaceData/SurfaceData.h index 9faa03f921..45e2ad3abb 100644 --- a/Code/Framework/AzFramework/AzFramework/SurfaceData/SurfaceData.h +++ b/Code/Framework/AzFramework/AzFramework/SurfaceData/SurfaceData.h @@ -10,13 +10,19 @@ #include #include #include -#include +#include namespace AzFramework::SurfaceData { namespace Constants { static constexpr const char* s_unassignedTagName = "(unassigned)"; + + //! The maximum number of surface weights that we can store. + //! For performance reasons, we want to limit this so that we can preallocate the max size in advance. + //! The current number is chosen to be higher than expected needs, but small enough to avoid being excessively wasteful. + //! (Dynamic structures would end up taking more memory than what we're preallocating) + static constexpr size_t MaxSurfaceWeights = 16; } struct SurfaceTagWeight @@ -70,7 +76,7 @@ namespace AzFramework::SurfaceData } }; - using SurfaceTagWeightList = AZStd::vector; + using SurfaceTagWeightList = AZStd::fixed_vector; struct SurfacePoint final { diff --git a/Gems/SurfaceData/Code/Include/SurfaceData/SurfaceDataTypes.h b/Gems/SurfaceData/Code/Include/SurfaceData/SurfaceDataTypes.h index 943c45bb6a..1d49fff3dc 100644 --- a/Gems/SurfaceData/Code/Include/SurfaceData/SurfaceDataTypes.h +++ b/Gems/SurfaceData/Code/Include/SurfaceData/SurfaceDataTypes.h @@ -29,12 +29,6 @@ namespace SurfaceData class SurfaceTagWeights { public: - //! The maximum number of surface weights that we can store. - //! For performance reasons, we want to limit this so that we can preallocate the max size in advance. - //! The current number is chosen to be higher than expected needs, but small enough to avoid being excessively wasteful. - //! (Dynamic structures would end up taking more memory than what we're preallocating) - static inline constexpr size_t MaxSurfaceWeights = 16; - SurfaceTagWeights() = default; //! Construct a collection of SurfaceTagWeights from the given SurfaceTagWeightList. @@ -65,7 +59,7 @@ namespace SurfaceData // early-out once we pass the location for the entry instead of always searching every entry. if (weightItr->m_surfaceType > tag) { - if (m_weights.size() != MaxSurfaceWeights) + if (m_weights.size() != AzFramework::SurfaceData::Constants::MaxSurfaceWeights) { // We didn't find the surface type, so add the new entry in sorted order. m_weights.insert(weightItr, { tag, weight }); @@ -85,7 +79,7 @@ namespace SurfaceData } // We didn't find the surface weight, and the sort order for it is at the end, so add it to the back of the list. - if (m_weights.size() != MaxSurfaceWeights) + if (m_weights.size() != AzFramework::SurfaceData::Constants::MaxSurfaceWeights) { m_weights.emplace_back(tag, weight); } @@ -188,7 +182,7 @@ namespace SurfaceData //! @return The pointer to the tag that's found, or end() if it wasn't found. const AzFramework::SurfaceData::SurfaceTagWeight* FindTag(AZ::Crc32 tag) const; - AZStd::fixed_vector m_weights; + AZStd::fixed_vector m_weights; }; //! SurfacePointList stores a collection of surface point data, which consists of positions, normals, and surface tag weights. diff --git a/Gems/SurfaceData/Code/Source/SurfaceDataTypes.cpp b/Gems/SurfaceData/Code/Source/SurfaceDataTypes.cpp index a25273896e..3d6378aa7c 100644 --- a/Gems/SurfaceData/Code/Source/SurfaceDataTypes.cpp +++ b/Gems/SurfaceData/Code/Source/SurfaceDataTypes.cpp @@ -42,7 +42,7 @@ namespace SurfaceData AzFramework::SurfaceData::SurfaceTagWeightList SurfaceTagWeights::GetSurfaceTagWeightList() const { AzFramework::SurfaceData::SurfaceTagWeightList weights; - weights.reserve(m_weights.size()); + for (auto& weight : m_weights) { weights.emplace_back(weight); diff --git a/Gems/SurfaceData/Code/Tests/SurfaceDataBenchmarks.cpp b/Gems/SurfaceData/Code/Tests/SurfaceDataBenchmarks.cpp index df011113da..becb91f0ce 100644 --- a/Gems/SurfaceData/Code/Tests/SurfaceDataBenchmarks.cpp +++ b/Gems/SurfaceData/Code/Tests/SurfaceDataBenchmarks.cpp @@ -275,7 +275,7 @@ namespace UnitTest { AZ_PROFILE_FUNCTION(Entity); - AZ::Crc32 tags[SurfaceData::SurfaceTagWeights::MaxSurfaceWeights]; + AZ::Crc32 tags[AzFramework::SurfaceData::Constants::MaxSurfaceWeights]; AZ::SimpleLcgRandom randomGenerator(1234567); // Declare this outside the loop so that we aren't benchmarking creation and destruction. @@ -316,7 +316,7 @@ namespace UnitTest { AZ_PROFILE_FUNCTION(Entity); - AZ::Crc32 tags[SurfaceData::SurfaceTagWeights::MaxSurfaceWeights]; + AZ::Crc32 tags[AzFramework::SurfaceData::Constants::MaxSurfaceWeights]; AZ::SimpleLcgRandom randomGenerator(1234567); // Declare this outside the loop so that we aren't benchmarking creation and destruction. diff --git a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp index 8976faadac..6ce83094e4 100644 --- a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp +++ b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp @@ -177,6 +177,193 @@ bool TerrainSystem::InWorldBounds(float x, float y) const 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; +} + +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; @@ -315,6 +502,39 @@ bool TerrainSystem::GetIsHoleFromFloats(float x, float y, Sampler sampler) const 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); @@ -471,6 +691,35 @@ AZ::EntityId TerrainSystem::FindBestAreaEntityAtPosition(float x, float y, AZ::A 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, @@ -551,13 +800,17 @@ void TerrainSystem::ProcessHeightsFromList( return; } + AZStd::vector terrainExists(inPositions.size()); + AZStd::vector heights(inPositions.size()); + + GetHeightsSynchronous(inPositions, sampleFilter, heights, terrainExists); + AzFramework::SurfaceData::SurfacePoint surfacePoint; - for (const auto& position : inPositions) + for (size_t i = 0; i < inPositions.size(); i++) { - bool terrainExists = false; - surfacePoint.m_position = position; - surfacePoint.m_position.SetZ(GetHeight(position, sampleFilter, &terrainExists)); - perPositionCallback(surfacePoint, terrainExists); + surfacePoint.m_position = inPositions[i]; + surfacePoint.m_position.SetZ(heights[i]); + perPositionCallback(surfacePoint, terrainExists[i]); } } @@ -571,13 +824,17 @@ void TerrainSystem::ProcessNormalsFromList( return; } + AZStd::vector terrainExists(inPositions.size()); + AZStd::vector normals(inPositions.size()); + + GetNormalsSynchronous(inPositions, sampleFilter, normals, terrainExists); + AzFramework::SurfaceData::SurfacePoint surfacePoint; - for (const auto& position : inPositions) + for (size_t i = 0; i < inPositions.size(); i++) { - bool terrainExists = false; - surfacePoint.m_position = position; - surfacePoint.m_normal = GetNormal(position, sampleFilter, &terrainExists); - perPositionCallback(surfacePoint, terrainExists); + surfacePoint.m_position = inPositions[i]; + surfacePoint.m_normal = AZStd::move(normals[i]); + perPositionCallback(surfacePoint, terrainExists[i]); } } @@ -591,13 +848,17 @@ void TerrainSystem::ProcessSurfaceWeightsFromList( return; } + AZStd::vector outSurfaceWeightsList(inPositions.size()); + AZStd::vector terrainExists(inPositions.size()); + + GetOrderedSurfaceWeightsFromList(inPositions, sampleFilter, outSurfaceWeightsList, terrainExists); + AzFramework::SurfaceData::SurfacePoint surfacePoint; - for (const auto& position : inPositions) + for (size_t i = 0; i < inPositions.size(); i++) { - bool terrainExists = false; - surfacePoint.m_position = position; - GetSurfaceWeights(position, surfacePoint.m_surfaceTags, sampleFilter, &terrainExists); - perPositionCallback(surfacePoint, terrainExists); + surfacePoint.m_position = inPositions[i]; + surfacePoint.m_surfaceTags = AZStd::move(outSurfaceWeightsList[i]); + perPositionCallback(surfacePoint, terrainExists[i]); } } @@ -611,13 +872,26 @@ void TerrainSystem::ProcessSurfacePointsFromList( 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 (const auto& position : inPositions) + for (size_t i = 0; i < inPositions.size(); i++) { - bool terrainExists = false; - surfacePoint.m_position = position; - GetSurfacePoint(position, surfacePoint, sampleFilter, &terrainExists); - perPositionCallback(surfacePoint, terrainExists); + 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]); } } @@ -723,20 +997,23 @@ void TerrainSystem::ProcessHeightsFromRegion( return; } - const size_t numSamplesX = aznumeric_cast(ceil(inRegion.GetExtents().GetX() / stepSize.GetX())); - const size_t numSamplesY = aznumeric_cast(ceil(inRegion.GetExtents().GetY() / stepSize.GetY())); + 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; y < numSamplesY; y++) + for (size_t y = 0, i = 0; y < numSamplesY; y++) { - float fy = aznumeric_cast(inRegion.GetMin().GetY() + (y * stepSize.GetY())); for (size_t x = 0; x < numSamplesX; x++) { - bool terrainExists = false; - float fx = aznumeric_cast(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); + surfacePoint.m_position.Set(inPositions[i].GetX(), inPositions[i].GetY(), heights[i]); + perPositionCallback(x, y, surfacePoint, terrainExists[i]); + i++; } } } @@ -753,20 +1030,24 @@ void TerrainSystem::ProcessNormalsFromRegion( return; } - const size_t numSamplesX = aznumeric_cast(ceil(inRegion.GetExtents().GetX() / stepSize.GetX())); - const size_t numSamplesY = aznumeric_cast(ceil(inRegion.GetExtents().GetY() / stepSize.GetY())); + 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; y < numSamplesY; y++) + for (size_t y = 0, i = 0; y < numSamplesY; y++) { - float fy = aznumeric_cast(inRegion.GetMin().GetY() + (y * stepSize.GetY())); for (size_t x = 0; x < numSamplesX; x++) { - bool terrainExists = false; - float fx = aznumeric_cast(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); + 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++; } } } @@ -783,20 +1064,24 @@ void TerrainSystem::ProcessSurfaceWeightsFromRegion( return; } - const size_t numSamplesX = aznumeric_cast(ceil(inRegion.GetExtents().GetX() / stepSize.GetX())); - const size_t numSamplesY = aznumeric_cast(ceil(inRegion.GetExtents().GetY() / stepSize.GetY())); + 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; y < numSamplesY; y++) + for (size_t y = 0, i = 0; y < numSamplesY; y++) { - float fy = aznumeric_cast(inRegion.GetMin().GetY() + (y * stepSize.GetY())); for (size_t x = 0; x < numSamplesX; x++) { - bool terrainExists = false; - float fx = aznumeric_cast(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); + 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++; } } } @@ -813,20 +1098,33 @@ void TerrainSystem::ProcessSurfacePointsFromRegion( return; } - const size_t numSamplesX = aznumeric_cast(ceil(inRegion.GetExtents().GetX() / stepSize.GetX())); - const size_t numSamplesY = aznumeric_cast(ceil(inRegion.GetExtents().GetY() / stepSize.GetY())); + 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; y < numSamplesY; y++) + for (size_t y = 0, i = 0; y < numSamplesY; y++) { - float fy = aznumeric_cast(inRegion.GetMin().GetY() + (y * stepSize.GetY())); for (size_t x = 0; x < numSamplesX; x++) { - bool terrainExists = false; - float fx = aznumeric_cast(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); + 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++; } } } diff --git a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h index e0457b80af..7895ab96cc 100644 --- a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h +++ b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h @@ -207,6 +207,38 @@ namespace Terrain float GetTerrainAreaHeight(float x, float y, bool& terrainExists) const; AZ::Vector3 GetNormalSynchronous(float x, float y, Sampler sampler, bool* terrainExistsPtr) const; + typedef AZStd::function inPositions, + AZStd::span outPositions, + AZStd::span outTerrainExists, + AZStd::span outSurfaceWeights, + AZ::EntityId areaId)> BulkQueriesCallback; + + void GetHeightsSynchronous( + const AZStd::span& inPositions, + Sampler sampler, AZStd::span heights, + AZStd::span terrainExists) const; + void GetNormalsSynchronous( + const AZStd::span& inPositions, + Sampler sampler, AZStd::span normals, + AZStd::span terrainExists) const; + void GetOrderedSurfaceWeightsFromList( + const AZStd::span& inPositions, Sampler sampler, + AZStd::span outSurfaceWeightsList, + AZStd::span terrainExists) const; + void MakeBulkQueries( + const AZStd::span inPositions, + AZStd::span outPositions, + AZStd::span outTerrainExists, + AZStd::span outSurfaceWieghts, + BulkQueriesCallback queryCallback) const; + void GenerateQueryPositions(const AZStd::span& inPositions, + AZStd::vector& outPositions, + Sampler sampler) const; + AZStd::vector GenerateInputPositionsFromRegion( + const AZ::Aabb& inRegion, + const AZ::Vector2& stepSize) const; + // AZ::TickBus::Handler overrides ... void OnTick(float deltaTime, AZ::ScriptTimePoint time) override; diff --git a/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp b/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp index 2c01a3d455..1277e9ed20 100644 --- a/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp +++ b/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp @@ -158,6 +158,15 @@ namespace UnitTest // Let the test function modify these values based on the needs of the specific test. mockHeights(outPosition, terrainExists); }); + ON_CALL(*m_terrainAreaHeightRequests, GetHeights) + .WillByDefault( + [mockHeights](AZStd::span inOutPositionList, AZStd::span terrainExistsList) + { + for (int i = 0; i < inOutPositionList.size(); i++) + { + mockHeights(inOutPositionList[i], terrainExistsList[i]); + } + }); ActivateEntity(entity.get()); return entity; @@ -184,9 +193,9 @@ namespace UnitTest tagWeight3.m_weight = 0.3f; expectedTags.push_back(tagWeight3); - m_terrainAreaSurfaceRequests = AZStd::make_unique>(entity->GetId()); - ON_CALL(*m_terrainAreaSurfaceRequests, GetSurfaceWeights).WillByDefault( - [tagWeight1, tagWeight2, tagWeight3](const AZ::Vector3& position, AzFramework::SurfaceData::SurfaceTagWeightList& surfaceWeights) + auto mockGetSurfaceWeights = [tagWeight1, tagWeight2, tagWeight3]( + const AZ::Vector3& position, + AzFramework::SurfaceData::SurfaceTagWeightList& surfaceWeights) { surfaceWeights.clear(); float absYPos = fabsf(position.GetY()); @@ -202,6 +211,19 @@ namespace UnitTest { surfaceWeights.push_back(tagWeight3); } + }; + + m_terrainAreaSurfaceRequests = AZStd::make_unique>(entity->GetId()); + ON_CALL(*m_terrainAreaSurfaceRequests, GetSurfaceWeights).WillByDefault(mockGetSurfaceWeights); + ON_CALL(*m_terrainAreaSurfaceRequests, GetSurfaceWeightsFromList).WillByDefault( + [mockGetSurfaceWeights]( + AZStd::span inPositionList, + AZStd::span outSurfaceWeightsList) + { + for (size_t i = 0; i < inPositionList.size(); i++) + { + mockGetSurfaceWeights(inPositionList[i], outSurfaceWeightsList[i]); + } } ); }