From 5dc442fcb0f2946cd104849b9bc814be134b9426 Mon Sep 17 00:00:00 2001 From: amzn-sj Date: Wed, 12 Jan 2022 12:07:57 -0800 Subject: [PATCH] [Terrain] First pass of the ProcessList and ProcessRegion APIs for retrieving surface data (#6729) * [Terrain] First pass of the ProcessList and ProcessRegion APIs for retrieving surface data Signed-off-by: amzn-sj * Add a couple of more tests. The expected values were plugged in based on the values generated by the brute force approach. Signed-off-by: amzn-sj * Move some declarations out of loops since they can be reused. Signed-off-by: amzn-sj * Update all the per position callbacks to pass SurfacePoint refs. Construct only one SurfacePoint object outside the loop which can be reused. Signed-off-by: amzn-sj * Update tests to use the new per position callbacks Signed-off-by: amzn-sj * Add ProcessRegion functions to the terrain benchmark. Signed-off-by: amzn-sj * Change C style static casts to aznumeric_cast. Add maybe_unused to unused params in benchmarks. Signed-off-by: amzn-sj * Update the ProcessList API functions to use array_view instead of a vector. This includes some additional changes to satisfy build dependencies. Signed-off-by: amzn-sj * Add ProcessList API functions to benchmarks Signed-off-by: amzn-sj * Update the ProcessList API functions to take Vector2 as input positions Signed-off-by: amzn-sj * Revert changes to AtomCore library split. Add partial implementation of span(mostly just copied over from array_view) to AzCore std containers. Signed-off-by: amzn-sj * Adding some const/non-const overloads that were missing in span Signed-off-by: amzn-sj * Move input position list generation to a function Signed-off-by: amzn-sj * Bring back Vector3 version of ProcessList functions. Rename Vector2 version to follow similar pattern as the Get functions. Signed-off-by: amzn-sj * Split span.h into .h/.inl files Signed-off-by: amzn-sj * Add [mayby_unused] for unused parameters to fix build errors Signed-off-by: amzn-sj --- .../AzCore/AzCore/std/azstd_files.cmake | 2 + .../AzCore/AzCore/std/containers/span.h | 137 ++++++++ .../AzCore/AzCore/std/containers/span.inl | 150 ++++++++ .../Terrain/TerrainDataRequestBus.h | 50 +++ .../Mocks/Terrain/MockTerrainDataRequestBus.h | 24 ++ .../Source/TerrainSystem/TerrainSystem.cpp | 282 +++++++++++++-- .../Code/Source/TerrainSystem/TerrainSystem.h | 47 +++ .../Code/Tests/TerrainSystemBenchmarks.cpp | 262 ++++++++++++++ Gems/Terrain/Code/Tests/TerrainSystemTest.cpp | 330 ++++++++++++++++++ 9 files changed, 1251 insertions(+), 33 deletions(-) create mode 100644 Code/Framework/AzCore/AzCore/std/containers/span.h create mode 100644 Code/Framework/AzCore/AzCore/std/containers/span.inl diff --git a/Code/Framework/AzCore/AzCore/std/azstd_files.cmake b/Code/Framework/AzCore/AzCore/std/azstd_files.cmake index 2746489f8c..d516a56295 100644 --- a/Code/Framework/AzCore/AzCore/std/azstd_files.cmake +++ b/Code/Framework/AzCore/AzCore/std/azstd_files.cmake @@ -64,6 +64,8 @@ set(FILES containers/rbtree.h containers/ring_buffer.h containers/set.h + containers/span.h + containers/span.inl containers/stack.h containers/unordered_map.h containers/unordered_set.h diff --git a/Code/Framework/AzCore/AzCore/std/containers/span.h b/Code/Framework/AzCore/AzCore/std/containers/span.h new file mode 100644 index 0000000000..8f68e921d8 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/std/containers/span.h @@ -0,0 +1,137 @@ +/* + * 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 + * + */ +#pragma once + +#include +#include +#include + +namespace AZStd +{ + /** + * First pass partial implementation of span copied over from array_view. It + * returns non-const iterator/pointers. first(), last(), and subspan() + * are yet to be implemented. It does not maintain storage for the data, + * but just holds pointers to mark the beginning and end of the array. + * It can be conveniently constructed from a variety of other container + * types like array, vector, and fixed_vector. + * + * Example: + * Given "void Func(AZStd::span a) {...}" you can call... + * - Func({1,2,3}); + * - AZStd::array a = {1,2,3}; + * Func(a); + * - AZStd::vector v = {1,2,3}; + * Func(v); + * - AZStd::fixed_vector fv = {1,2,3}; + * Func(fv); + * + * Since the span does not copy and store any data, it is only valid as long as the data used to create it is valid. + */ + template + class span final + { + public: + using value_type = Element; + + using pointer = value_type*; + using const_pointer = const value_type*; + + using reference = value_type&; + using const_reference = const value_type&; + + using size_type = AZStd::size_t; + using difference_type = AZStd::ptrdiff_t; + + using iterator = value_type*; + using const_iterator = const value_type*; + using reverse_iterator = AZStd::reverse_iterator; + using const_reverse_iterator = AZStd::reverse_iterator; + + constexpr span(); + + ~span() = default; + + constexpr span(pointer s, size_type length); + + constexpr span(pointer first, const_pointer last); + + // We explicitly delete this constructor because it's too easy to accidentally + // create a span to just the first element instead of an entire array. + constexpr span(const_pointer s) = delete; + + template + constexpr span(AZStd::array& data); + + constexpr span(AZStd::vector& data); + + template + constexpr span(AZStd::fixed_vector& data); + + template + constexpr span(const AZStd::array& data); + + constexpr span(const AZStd::vector& data); + + template + constexpr span(const AZStd::fixed_vector& data); + + constexpr span(const span&) = default; + + constexpr span(span&& other); + + constexpr span& operator=(const span& other) = default; + + constexpr span& operator=(span&& other); + + constexpr size_type size() const; + + constexpr bool empty() const; + + constexpr pointer data(); + constexpr const_pointer data() const; + + constexpr const_reference operator[](size_type index) const; + constexpr reference operator[](size_type index); + + constexpr void erase(); + + constexpr iterator begin(); + constexpr iterator end(); + constexpr const_iterator begin() const; + constexpr const_iterator end() const; + + constexpr const_iterator cbegin() const; + constexpr const_iterator cend() const; + + constexpr reverse_iterator rbegin(); + constexpr reverse_iterator rend(); + constexpr const_reverse_iterator rbegin() const; + constexpr const_reverse_iterator rend() const; + + constexpr const_reverse_iterator crbegin() const; + constexpr const_reverse_iterator crend() const; + + friend bool operator==(span lhs, span rhs) + { + return lhs.m_begin == rhs.m_begin && lhs.m_end == rhs.m_end; + } + + friend bool operator!=(span lhs, span rhs) { return !(lhs == rhs); } + friend bool operator< (span lhs, span rhs) { return lhs.m_begin < rhs.m_begin || lhs.m_begin == rhs.m_begin && lhs.m_end < rhs.m_end; } + friend bool operator> (span lhs, span rhs) { return lhs.m_begin > rhs.m_begin || lhs.m_begin == rhs.m_begin && lhs.m_end > rhs.m_end; } + friend bool operator<=(span lhs, span rhs) { return lhs == rhs || lhs < rhs; } + friend bool operator>=(span lhs, span rhs) { return lhs == rhs || lhs > rhs; } + + private: + pointer m_begin; + pointer m_end; + }; +} // namespace AZStd + +#include diff --git a/Code/Framework/AzCore/AzCore/std/containers/span.inl b/Code/Framework/AzCore/AzCore/std/containers/span.inl new file mode 100644 index 0000000000..c33e4a7227 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/std/containers/span.inl @@ -0,0 +1,150 @@ +/* + * 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 + * + */ +#pragma once + +namespace AZStd +{ + template + inline constexpr span::span() + : m_begin(nullptr) + , m_end(nullptr) + { } + + template + inline constexpr span::span(pointer s, size_type length) + : m_begin(s) + , m_end(m_begin + length) + { + if (length == 0) erase(); + } + + template + inline constexpr span::span(pointer first, const_pointer last) + : m_begin(first) + , m_end(last) + { } + + template + template + inline constexpr span::span(AZStd::array& data) + : m_begin(data.data()) + , m_end(m_begin + data.size()) + { } + + template + inline constexpr span::span(AZStd::vector& data) + : m_begin(data.data()) + , m_end(m_begin + data.size()) + { } + + template + template + inline constexpr span::span(AZStd::fixed_vector& data) + : m_begin(data.data()) + , m_end(m_begin + data.size()) + { } + + template + template + inline constexpr span::span(const AZStd::array& data) + : m_begin(data.data()) + , m_end(m_begin + data.size()) + { } + + template + inline constexpr span::span(const AZStd::vector& data) + : m_begin(data.data()) + , m_end(m_begin + data.size()) + { } + + template + template + inline constexpr span::span(const AZStd::fixed_vector& data) + : m_begin(data.data()) + , m_end(m_begin + data.size()) + { } + + template + inline constexpr span::span(span&& other) + : span(other.m_begin, other.m_end) + { +#if AZ_DEBUG_BUILD // Clearing the original pointers isn't necessary, but is good for debugging + other.m_begin = nullptr; + other.m_end = nullptr; +#endif + } + + template + inline constexpr AZStd::size_t span::size() const { return m_end - m_begin; } + + template + inline constexpr bool span::empty() const { return m_end == m_begin; } + + template + inline constexpr Element* span::data() { return m_begin; } + + template + inline constexpr const Element* span::data() const { return m_begin; } + + template + inline constexpr span& span::operator=(span&& other) + { + m_begin = other.m_begin; + m_end = other.m_end; +#if AZ_DEBUG_BUILD // Clearing the original pointers isn't necessary, but is good for debugging + other.m_begin = nullptr; + other.m_end = nullptr; +#endif + return *this; + } + + template + inline constexpr const Element& span::operator[](AZStd::size_t index) const + { + AZ_Assert(index < size(), "index value is out of range"); + return m_begin[index]; + } + + template + inline constexpr Element& span::operator[](AZStd::size_t index) + { + AZ_Assert(index < size(), "index value is out of range"); + return m_begin[index]; + } + + template + inline constexpr void span::erase() { m_begin = m_end = nullptr; } + + template + inline constexpr Element* span::begin() { return m_begin; } + template + inline constexpr Element* span::end() { return m_end; } + template + inline constexpr const Element* span::begin() const { return m_begin; } + template + inline constexpr const Element* span::end() const { return m_end; } + + template + inline constexpr const Element* span::cbegin() const { return m_begin; } + template + inline constexpr const Element* span::cend() const { return m_end; } + + template + inline constexpr AZStd::reverse_iterator span::rbegin() { return AZStd::reverse_iterator(m_end); } + template + inline constexpr AZStd::reverse_iterator span::rend() { return AZStd::reverse_iterator(m_begin); } + template + inline constexpr AZStd::reverse_iterator span::rbegin() const { return AZStd::reverse_iterator(m_end); } + template + inline constexpr AZStd::reverse_iterator span::rend() const { return AZStd::reverse_iterator(m_begin); } + + template + inline constexpr AZStd::reverse_iterator span::crbegin() const { return AZStd::reverse_iterator(cend()); } + template + inline constexpr AZStd::reverse_iterator span::crend() const { return AZStd::reverse_iterator(cbegin()); } +} // namespace AZStd diff --git a/Code/Framework/AzFramework/AzFramework/Terrain/TerrainDataRequestBus.h b/Code/Framework/AzFramework/AzFramework/Terrain/TerrainDataRequestBus.h index 2572a07494..0d16bf3460 100644 --- a/Code/Framework/AzFramework/AzFramework/Terrain/TerrainDataRequestBus.h +++ b/Code/Framework/AzFramework/AzFramework/Terrain/TerrainDataRequestBus.h @@ -11,12 +11,15 @@ #include #include #include +#include #include namespace AzFramework { namespace Terrain { + typedef AZStd::function SurfacePointRegionFillCallback; + typedef AZStd::function SurfacePointListFillCallback; //! Shared interface for terrain system implementations class TerrainDataRequests @@ -131,6 +134,53 @@ namespace AzFramework Sampler sampleFilter = Sampler::DEFAULT, bool* terrainExistsPtr = nullptr) const = 0; + //! Given a list of XY coordinates, call the provided callback function with surface data corresponding to each + //! XY coordinate in the list. + virtual void ProcessHeightsFromList(const AZStd::span& inPositions, + SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT) const = 0; + virtual void ProcessNormalsFromList(const AZStd::span& inPositions, + SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT) const = 0; + virtual void ProcessSurfaceWeightsFromList(const AZStd::span& inPositions, + SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT) const = 0; + virtual void ProcessSurfacePointsFromList(const AZStd::span& inPositions, + SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT) const = 0; + virtual void ProcessHeightsFromListOfVector2(const AZStd::span& inPositions, + SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT) const = 0; + virtual void ProcessNormalsFromListOfVector2(const AZStd::span& inPositions, + SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT) const = 0; + virtual void ProcessSurfaceWeightsFromListOfVector2(const AZStd::span& inPositions, + SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT) const = 0; + virtual void ProcessSurfacePointsFromListOfVector2(const AZStd::span& inPositions, + SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT) const = 0; + + //! Given a region(aabb) and a step size, call the provided callback function with surface data corresponding to the + //! coordinates in the region. + virtual void ProcessHeightsFromRegion(const AZ::Aabb& inRegion, + const AZ::Vector2& stepSize, + SurfacePointRegionFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT) const = 0; + virtual void ProcessNormalsFromRegion(const AZ::Aabb& inRegion, + const AZ::Vector2& stepSize, + SurfacePointRegionFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT) const = 0; + virtual void ProcessSurfaceWeightsFromRegion(const AZ::Aabb& inRegion, + const AZ::Vector2& stepSize, + SurfacePointRegionFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT) const = 0; + virtual void ProcessSurfacePointsFromRegion(const AZ::Aabb& inRegion, + const AZ::Vector2& stepSize, + SurfacePointRegionFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT) const = 0; + + private: // Private variations of the GetSurfacePoint API exposed to BehaviorContext that returns a value instead of // using an "out" parameter. The "out" parameter is useful for reusing memory allocated in SurfacePoint when diff --git a/Code/Framework/AzFramework/Tests/Mocks/Terrain/MockTerrainDataRequestBus.h b/Code/Framework/AzFramework/Tests/Mocks/Terrain/MockTerrainDataRequestBus.h index dbce11d639..f3a6cc07b3 100644 --- a/Code/Framework/AzFramework/Tests/Mocks/Terrain/MockTerrainDataRequestBus.h +++ b/Code/Framework/AzFramework/Tests/Mocks/Terrain/MockTerrainDataRequestBus.h @@ -76,5 +76,29 @@ namespace UnitTest GetSurfacePointFromVector2, void(const AZ::Vector2&, AzFramework::SurfaceData::SurfacePoint&, Sampler, bool*)); MOCK_CONST_METHOD5( GetSurfacePointFromFloats, void(float, float, AzFramework::SurfaceData::SurfacePoint&, Sampler, bool*)); + MOCK_CONST_METHOD3( + ProcessHeightsFromList, void(const AZStd::span&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler)); + MOCK_CONST_METHOD3( + ProcessNormalsFromList, void(const AZStd::span&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler)); + MOCK_CONST_METHOD3( + ProcessSurfaceWeightsFromList, void(const AZStd::span&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler)); + MOCK_CONST_METHOD3( + ProcessSurfacePointsFromList, void(const AZStd::span&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler)); + MOCK_CONST_METHOD3( + ProcessHeightsFromListOfVector2, void(const AZStd::span&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler)); + MOCK_CONST_METHOD3( + ProcessNormalsFromListOfVector2, void(const AZStd::span&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler)); + MOCK_CONST_METHOD3( + ProcessSurfaceWeightsFromListOfVector2, void(const AZStd::span&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler)); + MOCK_CONST_METHOD3( + ProcessSurfacePointsFromListOfVector2, void(const AZStd::span&, AzFramework::Terrain::SurfacePointListFillCallback, Sampler)); + MOCK_CONST_METHOD4( + ProcessHeightsFromRegion, void(const AZ::Aabb&, const AZ::Vector2&, AzFramework::Terrain::SurfacePointRegionFillCallback, Sampler)); + MOCK_CONST_METHOD4( + ProcessNormalsFromRegion, void(const AZ::Aabb&, const AZ::Vector2&, AzFramework::Terrain::SurfacePointRegionFillCallback, Sampler)); + MOCK_CONST_METHOD4( + ProcessSurfaceWeightsFromRegion, void(const AZ::Aabb&, const AZ::Vector2&, AzFramework::Terrain::SurfacePointRegionFillCallback, Sampler)); + MOCK_CONST_METHOD4( + ProcessSurfacePointsFromRegion, void(const AZ::Aabb&, const AZ::Vector2&, AzFramework::Terrain::SurfacePointRegionFillCallback, Sampler)); }; } // namespace UnitTest diff --git a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp index 7c8b6021af..2ecd13b5ad 100644 --- a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp +++ b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp @@ -530,9 +530,171 @@ const char* TerrainSystem::GetMaxSurfaceName( return ""; } -/* +void TerrainSystem::ProcessHeightsFromList( + const AZStd::span& inPositions, + AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter) const +{ + if (!perPositionCallback) + { + return; + } -void TerrainSystem::ProcessHeightsFromRegion(const AZ::Aabb& inRegion, const AZ::Vector2 stepSize, Sampler sampleFilter, SurfacePointRegionFillCallback perPositionCallback, TerrainDataReadyCallback onComplete) + 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& 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& 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& 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& 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& 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& 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& 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); + } +} + +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) @@ -540,30 +702,29 @@ void TerrainSystem::ProcessHeightsFromRegion(const AZ::Aabb& inRegion, const AZ: return; } - uint32_t numSamplesX = static_cast((inRegion.GetMax().GetX() - inRegion.GetMin().GetX()) / stepSize.GetX()); - uint32_t numSamplesY = static_cast((inRegion.GetMax().GetY() - inRegion.GetMin().GetY()) / stepSize.GetY()); - - for (uint32_t y = 0; y < numSamplesY; y++) + const size_t numSamplesX = aznumeric_cast(ceil(inRegion.GetExtents().GetX() / stepSize.GetX())); + const size_t numSamplesY = aznumeric_cast(ceil(inRegion.GetExtents().GetY() / stepSize.GetY())); + + AzFramework::SurfaceData::SurfacePoint surfacePoint; + for (size_t y = 0; y < numSamplesY; y++) { - for (uint32_t x = 0; x < numSamplesX; x++) + float fy = aznumeric_cast(inRegion.GetMin().GetY() + (y * stepSize.GetY())); + for (size_t x = 0; x < numSamplesX; x++) { - float fx = (float)(inRegion.GetMin().GetX() + (x * stepSize.GetX())); - float fy = (float)(inRegion.GetMin().GetY() + (y * stepSize.GetY())); - - SurfaceData::SurfacePoint surfacePoint; - GetHeight(AZ::Vector3(fx, fy, 0.0f), sampleFilter, surfacePoint.m_position); - perPositionCallback(surfacePoint, x, y); + 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); } } - - if (onComplete) - { - onComplete(); - } } - -void TerrainSystem::ProcessSurfacePointsFromRegion(const AZ::Aabb& inRegion, const AZ::Vector2 stepSize, Sampler sampleFilter, SurfacePointRegionFillCallback perPositionCallback, TerrainDataReadyCallback onComplete) +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) @@ -571,28 +732,83 @@ void TerrainSystem::ProcessSurfacePointsFromRegion(const AZ::Aabb& inRegion, con return; } - uint32_t numSamplesX = static_cast((inRegion.GetMax().GetX() - inRegion.GetMin().GetX()) / stepSize.GetX()); - uint32_t numSamplesY = static_cast((inRegion.GetMax().GetY() - inRegion.GetMin().GetY()) / stepSize.GetY()); + const size_t numSamplesX = aznumeric_cast(ceil(inRegion.GetExtents().GetX() / stepSize.GetX())); + const size_t numSamplesY = aznumeric_cast(ceil(inRegion.GetExtents().GetY() / stepSize.GetY())); - for (uint32_t y = 0; y < numSamplesY; y++) + AzFramework::SurfaceData::SurfacePoint surfacePoint; + for (size_t y = 0; y < numSamplesY; y++) { - for (uint32_t x = 0; x < numSamplesX; x++) + float fy = aznumeric_cast(inRegion.GetMin().GetY() + (y * stepSize.GetY())); + for (size_t x = 0; x < numSamplesX; x++) { - float fx = (float)(inRegion.GetMin().GetX() + (x * stepSize.GetX())); - float fy = (float)(inRegion.GetMin().GetY() + (y * stepSize.GetY())); - - SurfaceData::SurfacePoint surfacePoint; - GetSurfacePoint(AZ::Vector3(fx, fy, inRegion.GetMin().GetZ()), sampleFilter, surfacePoint); - perPositionCallback(surfacePoint, x, y); + 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); } } +} - if (onComplete) +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) { - onComplete(); + 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())); + + AzFramework::SurfaceData::SurfacePoint surfacePoint; + 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++) + { + 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); + } + } +} + +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(ceil(inRegion.GetExtents().GetX() / stepSize.GetX())); + const size_t numSamplesY = aznumeric_cast(ceil(inRegion.GetExtents().GetY() / stepSize.GetY())); + + AzFramework::SurfaceData::SurfacePoint surfacePoint; + 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++) + { + 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); + } } } -*/ void TerrainSystem::RegisterArea(AZ::EntityId areaId) { diff --git a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h index d7267476ed..7c6e0cd91e 100644 --- a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h +++ b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -135,6 +136,52 @@ namespace Terrain Sampler sampleFilter = Sampler::DEFAULT, bool* terrainExistsPtr = nullptr) const override; + //! Given a list of XY coordinates, call the provided callback function with surface data corresponding to each + //! XY coordinate in the list. + virtual void ProcessHeightsFromList(const AZStd::span& inPositions, + AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT) const override; + virtual void ProcessNormalsFromList(const AZStd::span& inPositions, + AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT) const override; + virtual void ProcessSurfaceWeightsFromList(const AZStd::span& inPositions, + AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT) const override; + virtual void ProcessSurfacePointsFromList(const AZStd::span& inPositions, + AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT) const override; + virtual void ProcessHeightsFromListOfVector2(const AZStd::span& inPositions, + AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT) const override; + virtual void ProcessNormalsFromListOfVector2(const AZStd::span& inPositions, + AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT) const override; + virtual void ProcessSurfaceWeightsFromListOfVector2(const AZStd::span& inPositions, + AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT) const override; + virtual void ProcessSurfacePointsFromListOfVector2(const AZStd::span& inPositions, + AzFramework::Terrain::SurfacePointListFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT) const override; + + //! Given a region(aabb) and a step size, call the provided callback function with surface data corresponding to the + //! coordinates in the region. + virtual void ProcessHeightsFromRegion(const AZ::Aabb& inRegion, + const AZ::Vector2& stepSize, + AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT) const override; + virtual void ProcessNormalsFromRegion(const AZ::Aabb& inRegion, + const AZ::Vector2& stepSize, + AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT) const override; + virtual void ProcessSurfaceWeightsFromRegion(const AZ::Aabb& inRegion, + const AZ::Vector2& stepSize, + AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT) const override; + virtual void ProcessSurfacePointsFromRegion(const AZ::Aabb& inRegion, + const AZ::Vector2& stepSize, + AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback, + Sampler sampleFilter = Sampler::DEFAULT) const override; + private: void ClampPosition(float x, float y, AZ::Vector2& outPosition, AZ::Vector2& normalizedDelta) const; diff --git a/Gems/Terrain/Code/Tests/TerrainSystemBenchmarks.cpp b/Gems/Terrain/Code/Tests/TerrainSystemBenchmarks.cpp index 3d0cb5222e..85dd5dc0c4 100644 --- a/Gems/Terrain/Code/Tests/TerrainSystemBenchmarks.cpp +++ b/Gems/Terrain/Code/Tests/TerrainSystemBenchmarks.cpp @@ -281,6 +281,22 @@ namespace UnitTest surfaceGradientShapeRequests.clear(); } + void GenerateInputPositionsList(const AZ::Vector2& queryResolution, const AZ::Aabb& worldBounds, AZStd::vector& positions) + { + const size_t numSamplesX = aznumeric_cast(ceil(worldBounds.GetExtents().GetX() / queryResolution.GetX())); + const size_t numSamplesY = aznumeric_cast(ceil(worldBounds.GetExtents().GetY() / queryResolution.GetY())); + + for (size_t y = 0; y < numSamplesY; y++) + { + float fy = aznumeric_cast(worldBounds.GetMin().GetY() + (y * queryResolution.GetY())); + for (size_t x = 0; x < numSamplesX; x++) + { + float fx = aznumeric_cast(worldBounds.GetMin().GetX() + (x * queryResolution.GetX())); + positions.emplace_back(fx, fy, 0.0f); + } + } + } + protected: AZStd::unique_ptr m_app; }; @@ -322,6 +338,72 @@ namespace UnitTest ->Args({ 4096, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) ->Unit(::benchmark::kMillisecond); + BENCHMARK_DEFINE_F(TerrainSystemBenchmarkFixture, BM_ProcessHeightsRegion)(benchmark::State& state) + { + // Run the benchmark + RunTerrainApiBenchmark( + state, + []([[maybe_unused]] const AZ::Vector2& queryResolution, const AZ::Aabb& worldBounds, + AzFramework::Terrain::TerrainDataRequests::Sampler sampler) + { + auto perPositionCallback = []([[maybe_unused]] size_t xIndex, [[maybe_unused]] size_t yIndex, + const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists) + { + benchmark::DoNotOptimize(surfacePoint.m_position.GetZ()); + }; + + AzFramework::Terrain::TerrainDataRequestBus::Broadcast( + &AzFramework::Terrain::TerrainDataRequests::ProcessHeightsFromRegion, worldBounds, queryResolution, perPositionCallback, sampler); + } + ); + } + + BENCHMARK_REGISTER_F(TerrainSystemBenchmarkFixture, BM_ProcessHeightsRegion) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 4096, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 4096, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 4096, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Unit(::benchmark::kMillisecond); + + BENCHMARK_DEFINE_F(TerrainSystemBenchmarkFixture, BM_ProcessHeightsList)(benchmark::State& state) + { + // Run the benchmark + RunTerrainApiBenchmark( + state, + [this]([[maybe_unused]] const AZ::Vector2& queryResolution, const AZ::Aabb& worldBounds, + AzFramework::Terrain::TerrainDataRequests::Sampler sampler) + { + AZStd::vector inPositions; + GenerateInputPositionsList(queryResolution, worldBounds, inPositions); + + auto perPositionCallback = [](const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists) + { + benchmark::DoNotOptimize(surfacePoint.m_position.GetZ()); + }; + + AzFramework::Terrain::TerrainDataRequestBus::Broadcast( + &AzFramework::Terrain::TerrainDataRequests::ProcessHeightsFromList, inPositions, perPositionCallback, sampler); + } + ); + } + + BENCHMARK_REGISTER_F(TerrainSystemBenchmarkFixture, BM_ProcessHeightsList) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 4096, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 4096, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 4096, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Unit(::benchmark::kMillisecond); + BENCHMARK_DEFINE_F(TerrainSystemBenchmarkFixture, BM_GetNormal)(benchmark::State& state) { // Run the benchmark @@ -353,6 +435,66 @@ namespace UnitTest ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) ->Unit(::benchmark::kMillisecond); + BENCHMARK_DEFINE_F(TerrainSystemBenchmarkFixture, BM_ProcessNormalsRegion)(benchmark::State& state) + { + // Run the benchmark + RunTerrainApiBenchmark( + state, + []([[maybe_unused]] const AZ::Vector2& queryResolution, const AZ::Aabb& worldBounds, + AzFramework::Terrain::TerrainDataRequests::Sampler sampler) + { + auto perPositionCallback = []([[maybe_unused]] size_t xIndex, [[maybe_unused]] size_t yIndex, + const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists) + { + benchmark::DoNotOptimize(surfacePoint.m_normal); + }; + + AzFramework::Terrain::TerrainDataRequestBus::Broadcast( + &AzFramework::Terrain::TerrainDataRequests::ProcessNormalsFromRegion, worldBounds, queryResolution, perPositionCallback, sampler); + } + ); + } + + BENCHMARK_REGISTER_F(TerrainSystemBenchmarkFixture, BM_ProcessNormalsRegion) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Unit(::benchmark::kMillisecond); + + BENCHMARK_DEFINE_F(TerrainSystemBenchmarkFixture, BM_ProcessNormalsList)(benchmark::State& state) + { + // Run the benchmark + RunTerrainApiBenchmark( + state, + [this]([[maybe_unused]] const AZ::Vector2& queryResolution, const AZ::Aabb& worldBounds, + AzFramework::Terrain::TerrainDataRequests::Sampler sampler) + { + AZStd::vector inPositions; + GenerateInputPositionsList(queryResolution, worldBounds, inPositions); + + auto perPositionCallback = [](const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists) + { + benchmark::DoNotOptimize(surfacePoint.m_normal); + }; + + AzFramework::Terrain::TerrainDataRequestBus::Broadcast( + &AzFramework::Terrain::TerrainDataRequests::ProcessNormalsFromList, inPositions, perPositionCallback, sampler); + } + ); + } + + BENCHMARK_REGISTER_F(TerrainSystemBenchmarkFixture, BM_ProcessNormalsList) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Unit(::benchmark::kMillisecond); + BENCHMARK_DEFINE_F(TerrainSystemBenchmarkFixture, BM_GetSurfaceWeights)(benchmark::State& state) { // Run the benchmark @@ -385,6 +527,66 @@ namespace UnitTest ->Args({ 2048, 4, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) ->Unit(::benchmark::kMillisecond); + BENCHMARK_DEFINE_F(TerrainSystemBenchmarkFixture, BM_ProcessSurfaceWeightsRegion)(benchmark::State& state) + { + // Run the benchmark + RunTerrainApiBenchmark( + state, + []([[maybe_unused]] const AZ::Vector2& queryResolution, const AZ::Aabb& worldBounds, + AzFramework::Terrain::TerrainDataRequests::Sampler sampler) + { + auto perPositionCallback = []([[maybe_unused]] size_t xIndex, [[maybe_unused]] size_t yIndex, + const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists) + { + benchmark::DoNotOptimize(surfacePoint.m_surfaceTags); + }; + + AzFramework::Terrain::TerrainDataRequestBus::Broadcast( + &AzFramework::Terrain::TerrainDataRequests::ProcessSurfaceWeightsFromRegion, worldBounds, queryResolution, perPositionCallback, sampler); + } + ); + } + + BENCHMARK_REGISTER_F(TerrainSystemBenchmarkFixture, BM_ProcessSurfaceWeightsRegion) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 1024, 2, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 2048, 2, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 1024, 4, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 2048, 4, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Unit(::benchmark::kMillisecond); + + BENCHMARK_DEFINE_F(TerrainSystemBenchmarkFixture, BM_ProcessSurfaceWeightsList)(benchmark::State& state) + { + // Run the benchmark + RunTerrainApiBenchmark( + state, + [this]([[maybe_unused]] const AZ::Vector2& queryResolution, const AZ::Aabb& worldBounds, + AzFramework::Terrain::TerrainDataRequests::Sampler sampler) + { + AZStd::vector inPositions; + GenerateInputPositionsList(queryResolution, worldBounds, inPositions); + + auto perPositionCallback = [](const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists) + { + benchmark::DoNotOptimize(surfacePoint.m_surfaceTags); + }; + + AzFramework::Terrain::TerrainDataRequestBus::Broadcast( + &AzFramework::Terrain::TerrainDataRequests::ProcessSurfaceWeightsFromList, inPositions, perPositionCallback, sampler); + } + ); + } + + BENCHMARK_REGISTER_F(TerrainSystemBenchmarkFixture, BM_ProcessSurfaceWeightsList) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 1024, 2, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 2048, 2, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 1024, 4, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 2048, 4, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Unit(::benchmark::kMillisecond); + BENCHMARK_DEFINE_F(TerrainSystemBenchmarkFixture, BM_GetSurfacePoints)(benchmark::State& state) { // Run the benchmark @@ -416,6 +618,66 @@ namespace UnitTest ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) ->Unit(::benchmark::kMillisecond); + + BENCHMARK_DEFINE_F(TerrainSystemBenchmarkFixture, BM_ProcessSurfacePointsRegion)(benchmark::State& state) + { + // Run the benchmark + RunTerrainApiBenchmark( + state, + []([[maybe_unused]] const AZ::Vector2& queryResolution, const AZ::Aabb& worldBounds, + AzFramework::Terrain::TerrainDataRequests::Sampler sampler) + { + auto perPositionCallback = []([[maybe_unused]] size_t xIndex, [[maybe_unused]] size_t yIndex, + const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists) + { + benchmark::DoNotOptimize(surfacePoint); + }; + + AzFramework::Terrain::TerrainDataRequestBus::Broadcast( + &AzFramework::Terrain::TerrainDataRequests::ProcessSurfacePointsFromRegion, worldBounds, queryResolution, perPositionCallback, sampler); + } + ); + } + + BENCHMARK_REGISTER_F(TerrainSystemBenchmarkFixture, BM_ProcessSurfacePointsRegion) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Unit(::benchmark::kMillisecond); + + BENCHMARK_DEFINE_F(TerrainSystemBenchmarkFixture, BM_ProcessSurfacePointsList)(benchmark::State& state) + { + // Run the benchmark + RunTerrainApiBenchmark( + state, + [this]([[maybe_unused]] const AZ::Vector2& queryResolution, const AZ::Aabb& worldBounds, + AzFramework::Terrain::TerrainDataRequests::Sampler sampler) + { + AZStd::vector inPositions; + GenerateInputPositionsList(queryResolution, worldBounds, inPositions); + + auto perPositionCallback = [](const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists) + { + benchmark::DoNotOptimize(surfacePoint); + }; + + AzFramework::Terrain::TerrainDataRequestBus::Broadcast( + &AzFramework::Terrain::TerrainDataRequests::ProcessSurfacePointsFromList, inPositions, perPositionCallback, sampler); + } + ); + } + + BENCHMARK_REGISTER_F(TerrainSystemBenchmarkFixture, BM_ProcessSurfacePointsList) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR) }) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP) }) + ->Args({ 1024, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Args({ 2048, 1, static_cast(AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT) }) + ->Unit(::benchmark::kMillisecond); #endif } diff --git a/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp b/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp index e5434c0a1d..a5798a3dad 100644 --- a/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp +++ b/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp @@ -41,6 +41,28 @@ namespace UnitTest float m_expectedHeight = 0.0f; }; + struct NormalTestPoint + { + AZ::Vector2 m_testLocation = AZ::Vector2::CreateZero(); + AZ::Vector3 m_expectedNormal = AZ::Vector3::CreateZero(); + }; + + struct HeightTestRegionPoints + { + size_t m_xIndex; + size_t m_yIndex; + float m_expectedHeight; + AZ::Vector2 m_testLocation = AZ::Vector2::CreateZero(); + }; + + struct NormalTestRegionPoints + { + size_t m_xIndex; + size_t m_yIndex; + AZ::Vector3 m_expectedNormal = AZ::Vector3::CreateZero(); + AZ::Vector2 m_testLocation = AZ::Vector2::CreateZero(); + }; + AZ::ComponentApplication m_app; AZStd::unique_ptr> m_boxShapeRequests; @@ -572,4 +594,312 @@ namespace UnitTest EXPECT_EQ(tagWeight.m_surfaceType, tagWeight1.m_surfaceType); EXPECT_NEAR(tagWeight.m_weight, tagWeight1.m_weight, 0.01f); } + + TEST_F(TerrainSystemTest, TerrainProcessHeightsFromListWithBilinearSamplers) + { + // This repeats the same test as TerrainHeightQueriesWithBilinearSamplersUseQueryGridToInterpolate + // The difference is that it tests the ProcessHeightsFromList variation. + + const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f); + const float amplitudeMeters = 10.0f; + const float frequencyMeters = 1.0f; + auto entity = CreateAndActivateMockTerrainLayerSpawner( + spawnerBox, + [amplitudeMeters, frequencyMeters](AZ::Vector3& position, bool& terrainExists) + { + // Our generated height will be X + Y. + float expectedHeight = position.GetX() + position.GetY(); + + // If either X or Y aren't evenly divisible by the query frequency, add a scaled value to our generated height. + // This will show up as an unexpected height "spike" if it gets used in any bilinear filter queries. + float unexpectedVariance = + amplitudeMeters * (fmodf(position.GetX(), frequencyMeters) + fmodf(position.GetY(), frequencyMeters)); + position.SetZ(expectedHeight + unexpectedVariance); + terrainExists = true; + }); + + // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 1 meter intervals. + const AZ::Vector2 queryResolution(frequencyMeters); + auto terrainSystem = CreateAndActivateTerrainSystem(queryResolution); + + // Test some points and verify that the results are the expected bilinear filtered result, + // whether they're in positive or negative space. + // (Z contains the the expected result for convenience). + const HeightTestPoint testPoints[] = { + + // Queries directly on grid points. These should return values of X + Y. + { AZ::Vector2(0.0f, 0.0f), 0.0f }, // Should return a height of 0 + 0 + { AZ::Vector2(1.0f, 0.0f), 1.0f }, // Should return a height of 1 + 0 + { AZ::Vector2(0.0f, 1.0f), 1.0f }, // Should return a height of 0 + 1 + { AZ::Vector2(1.0f, 1.0f), 2.0f }, // Should return a height of 1 + 1 + { AZ::Vector2(3.0f, 5.0f), 8.0f }, // Should return a height of 3 + 5 + + { AZ::Vector2(-1.0f, 0.0f), -1.0f }, // Should return a height of -1 + 0 + { AZ::Vector2(0.0f, -1.0f), -1.0f }, // Should return a height of 0 + -1 + { AZ::Vector2(-1.0f, -1.0f), -2.0f }, // Should return a height of -1 + -1 + { AZ::Vector2(-3.0f, -5.0f), -8.0f }, // Should return a height of -3 + -5 + + // Queries that are on a grid edge (one axis on the grid, the other somewhere in-between). + // These should just be a linear interpolation of the points, so it should still be X + Y. + + { AZ::Vector2(0.25f, 0.0f), 0.25f }, // Should return a height of -0.25 + 0 + { AZ::Vector2(3.75f, 0.0f), 3.75f }, // Should return a height of -3.75 + 0 + { AZ::Vector2(0.0f, 0.25f), 0.25f }, // Should return a height of 0 + -0.25 + { AZ::Vector2(0.0f, 3.75f), 3.75f }, // Should return a height of 0 + -3.75 + + { AZ::Vector2(2.0f, 3.75f), 5.75f }, // Should return a height of -2 + -3.75 + { AZ::Vector2(2.25f, 4.0f), 6.25f }, // Should return a height of -2.25 + -4 + + { AZ::Vector2(-0.25f, 0.0f), -0.25f }, // Should return a height of -0.25 + 0 + { AZ::Vector2(-3.75f, 0.0f), -3.75f }, // Should return a height of -3.75 + 0 + { AZ::Vector2(0.0f, -0.25f), -0.25f }, // Should return a height of 0 + -0.25 + { AZ::Vector2(0.0f, -3.75f), -3.75f }, // Should return a height of 0 + -3.75 + + { AZ::Vector2(-2.0f, -3.75f), -5.75f }, // Should return a height of -2 + -3.75 + { AZ::Vector2(-2.25f, -4.0f), -6.25f }, // Should return a height of -2.25 + -4 + + // Queries inside a grid square (both axes are in-between grid points) + // This is a full bilinear interpolation, but because we're using X + Y for our heights, the interpolated values + // should *still* be X + Y assuming the points were sampled correctly from the grid points. + + { AZ::Vector2(3.25f, 5.25f), 8.5f }, // Should return a height of 3.25 + 5.25 + { AZ::Vector2(7.71f, 9.74f), 17.45f }, // Should return a height of 7.71 + 9.74 + + { AZ::Vector2(-3.25f, -5.25f), -8.5f }, // Should return a height of -3.25 + -5.25 + { AZ::Vector2(-7.71f, -9.74f), -17.45f }, // Should return a height of -7.71 + -9.74 + }; + + auto perPositionCallback = [&testPoints](const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists){ + bool found = false; + for (auto& testPoint : testPoints) + { + if (testPoint.m_testLocation.GetX() == surfacePoint.m_position.GetX() && testPoint.m_testLocation.GetY() == surfacePoint.m_position.GetY()) + { + constexpr float epsilon = 0.0001f; + EXPECT_NEAR(surfacePoint.m_position.GetZ(), testPoint.m_expectedHeight, epsilon); + found = true; + break; + } + } + EXPECT_EQ(found, true); + }; + + AZStd::vector inPositions; + for (auto& testPoint : testPoints) + { + AZ::Vector3 position(testPoint.m_testLocation.GetX(), testPoint.m_testLocation.GetY(), 0.0f); + inPositions.push_back(position); + } + + terrainSystem->ProcessHeightsFromList(inPositions, perPositionCallback, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR); + } + + TEST_F(TerrainSystemTest, TerrainProcessNormalsFromListWithBilinearSamplers) + { + // Similar to TerrainProcessHeightsFromListWithBilinearSamplers but for normals + + const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f); + const float amplitudeMeters = 10.0f; + const float frequencyMeters = 1.0f; + auto entity = CreateAndActivateMockTerrainLayerSpawner( + spawnerBox, + [amplitudeMeters, frequencyMeters](AZ::Vector3& position, bool& terrainExists) + { + // Our generated height will be X + Y. + float expectedHeight = position.GetX() + position.GetY(); + + // If either X or Y aren't evenly divisible by the query frequency, add a scaled value to our generated height. + // This will show up as an unexpected height "spike" if it gets used in any bilinear filter queries. + float unexpectedVariance = + amplitudeMeters * (fmodf(position.GetX(), frequencyMeters) + fmodf(position.GetY(), frequencyMeters)); + position.SetZ(expectedHeight + unexpectedVariance); + terrainExists = true; + }); + + // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 1 meter intervals. + const AZ::Vector2 queryResolution(frequencyMeters); + auto terrainSystem = CreateAndActivateTerrainSystem(queryResolution); + + const NormalTestPoint testPoints[] = { + + { AZ::Vector2(0.0f, 0.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) }, + { AZ::Vector2(1.0f, 0.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) }, + { AZ::Vector2(0.0f, 1.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) }, + { AZ::Vector2(1.0f, 1.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) }, + { AZ::Vector2(3.0f, 5.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) }, + + { AZ::Vector2(-1.0f, 0.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) }, + { AZ::Vector2(0.0f, -1.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) }, + { AZ::Vector2(-1.0f, -1.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) }, + { AZ::Vector2(-3.0f, -5.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) }, + + { AZ::Vector2(0.25f, 0.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) }, + { AZ::Vector2(3.75f, 0.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) }, + { AZ::Vector2(0.0f, 0.25f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) }, + { AZ::Vector2(0.0f, 3.75f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) }, + + { AZ::Vector2(2.0f, 3.75f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) }, + { AZ::Vector2(2.25f, 4.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) }, + + { AZ::Vector2(-0.25f, 0.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) }, + { AZ::Vector2(-3.75f, 0.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) }, + { AZ::Vector2(0.0f, -0.25f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) }, + { AZ::Vector2(0.0f, -3.75f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) }, + + { AZ::Vector2(-2.0f, -3.75f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) }, + { AZ::Vector2(-2.25f, -4.0f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) }, + + { AZ::Vector2(3.25f, 5.25f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) }, + { AZ::Vector2(7.71f, 9.74f), AZ::Vector3(-0.0292f, 0.9991f, 0.0292f) }, + + { AZ::Vector2(-3.25f, -5.25f), AZ::Vector3(-0.5773f, -0.5773f, 0.5773f) }, + { AZ::Vector2(-7.71f, -9.74f), AZ::Vector3(-0.0366f, -0.9986f, 0.0366f) }, + }; + + auto perPositionCallback = [&testPoints](const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists){ + bool found = false; + for (auto& testPoint : testPoints) + { + if (testPoint.m_testLocation.GetX() == surfacePoint.m_position.GetX() && testPoint.m_testLocation.GetY() == surfacePoint.m_position.GetY()) + { + constexpr float epsilon = 0.0001f; + EXPECT_NEAR(surfacePoint.m_normal.GetX(), testPoint.m_expectedNormal.GetX(), epsilon); + EXPECT_NEAR(surfacePoint.m_normal.GetY(), testPoint.m_expectedNormal.GetY(), epsilon); + EXPECT_NEAR(surfacePoint.m_normal.GetZ(), testPoint.m_expectedNormal.GetZ(), epsilon); + found = true; + break; + } + } + EXPECT_EQ(found, true); + }; + + AZStd::vector inPositions; + for (auto& testPoint : testPoints) + { + AZ::Vector3 position(testPoint.m_testLocation.GetX(), testPoint.m_testLocation.GetY(), 0.0f); + inPositions.push_back(position); + } + + terrainSystem->ProcessNormalsFromList(inPositions, perPositionCallback, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR); + } + + TEST_F(TerrainSystemTest, TerrainProcessHeightsFromRegionWithBilinearSamplers) + { + // This repeats the same test as TerrainHeightQueriesWithBilinearSamplersUseQueryGridToInterpolate + // The difference is that it tests the ProcessHeightsFromList variation. + + const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f); + const float amplitudeMeters = 10.0f; + const float frequencyMeters = 1.0f; + auto entity = CreateAndActivateMockTerrainLayerSpawner( + spawnerBox, + [amplitudeMeters, frequencyMeters](AZ::Vector3& position, bool& terrainExists) + { + // Our generated height will be X + Y. + float expectedHeight = position.GetX() + position.GetY(); + + // If either X or Y aren't evenly divisible by the query frequency, add a scaled value to our generated height. + // This will show up as an unexpected height "spike" if it gets used in any bilinear filter queries. + float unexpectedVariance = + amplitudeMeters * (fmodf(position.GetX(), frequencyMeters) + fmodf(position.GetY(), frequencyMeters)); + position.SetZ(expectedHeight + unexpectedVariance); + terrainExists = true; + }); + + // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 1 meter intervals. + const AZ::Vector2 queryResolution(frequencyMeters); + auto terrainSystem = CreateAndActivateTerrainSystem(queryResolution); + + const AZ::Aabb testRegionBox = AZ::Aabb::CreateFromMinMaxValues(-1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f); + const AZ::Vector2 stepSize(1.0f); + + const HeightTestRegionPoints testPoints[] = { + { 0, 0, -2.0f, AZ::Vector2(-1.0f, -1.0f) }, + { 1, 0, -1.0f, AZ::Vector2(0.0f, -1.0f) }, + { 0, 1, -1.0f, AZ::Vector2(-1.0f, 0.0f) }, + { 1, 1, 0.0f, AZ::Vector2(0.0f, 0.0f) }, + }; + + auto perPositionCallback = [&testPoints](size_t xIndex, size_t yIndex, + const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists) + { + bool found = false; + for (auto& testPoint : testPoints) + { + if (testPoint.m_xIndex == xIndex && testPoint.m_yIndex == yIndex + && testPoint.m_testLocation.GetX() == surfacePoint.m_position.GetX() + && testPoint.m_testLocation.GetY() == surfacePoint.m_position.GetY()) + { + constexpr float epsilon = 0.0001f; + EXPECT_NEAR(surfacePoint.m_position.GetZ(), testPoint.m_expectedHeight, epsilon); + found = true; + break; + } + } + EXPECT_EQ(found, true); + }; + + terrainSystem->ProcessHeightsFromRegion(testRegionBox, stepSize, perPositionCallback, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR); + } + + TEST_F(TerrainSystemTest, TerrainProcessNormalsFromRegionWithBilinearSamplers) + { + // This repeats the same test as TerrainHeightQueriesWithBilinearSamplersUseQueryGridToInterpolate + // The difference is that it tests the ProcessHeightsFromList variation. + + const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f); + const float amplitudeMeters = 10.0f; + const float frequencyMeters = 1.0f; + auto entity = CreateAndActivateMockTerrainLayerSpawner( + spawnerBox, + [amplitudeMeters, frequencyMeters](AZ::Vector3& position, bool& terrainExists) + { + // Our generated height will be X + Y. + float expectedHeight = position.GetX() + position.GetY(); + + // If either X or Y aren't evenly divisible by the query frequency, add a scaled value to our generated height. + // This will show up as an unexpected height "spike" if it gets used in any bilinear filter queries. + float unexpectedVariance = + amplitudeMeters * (fmodf(position.GetX(), frequencyMeters) + fmodf(position.GetY(), frequencyMeters)); + position.SetZ(expectedHeight + unexpectedVariance); + terrainExists = true; + }); + + // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 1 meter intervals. + const AZ::Vector2 queryResolution(frequencyMeters); + auto terrainSystem = CreateAndActivateTerrainSystem(queryResolution); + + const AZ::Aabb testRegionBox = AZ::Aabb::CreateFromMinMaxValues(-1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f); + const AZ::Vector2 stepSize(1.0f); + + const NormalTestRegionPoints testPoints[] = { + { 0, 0, AZ::Vector3(-0.5773f, -0.5773f, 0.5773f), AZ::Vector2(-1.0f, -1.0f) }, + { 1, 0, AZ::Vector3(-0.5773f, -0.5773f, 0.5773f), AZ::Vector2(0.0f, -1.0f) }, + { 0, 1, AZ::Vector3(-0.5773f, -0.5773f, 0.5773f), AZ::Vector2(-1.0f, 0.0f) }, + { 1, 1, AZ::Vector3(-0.5773f, -0.5773f, 0.5773f), AZ::Vector2(0.0f, 0.0f) }, + }; + + auto perPositionCallback = [&testPoints](size_t xIndex, size_t yIndex, + const AzFramework::SurfaceData::SurfacePoint& surfacePoint, [[maybe_unused]] bool terrainExists) + { + bool found = false; + for (auto& testPoint : testPoints) + { + if (testPoint.m_xIndex == xIndex && testPoint.m_yIndex == yIndex + && testPoint.m_testLocation.GetX() == surfacePoint.m_position.GetX() + && testPoint.m_testLocation.GetY() == surfacePoint.m_position.GetY()) + { + constexpr float epsilon = 0.0001f; + EXPECT_NEAR(surfacePoint.m_normal.GetX(), testPoint.m_expectedNormal.GetX(), epsilon); + EXPECT_NEAR(surfacePoint.m_normal.GetY(), testPoint.m_expectedNormal.GetY(), epsilon); + EXPECT_NEAR(surfacePoint.m_normal.GetZ(), testPoint.m_expectedNormal.GetZ(), epsilon); + found = true; + break; + } + } + EXPECT_EQ(found, true); + }; + + terrainSystem->ProcessNormalsFromRegion(testRegionBox, stepSize, perPositionCallback, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR); + } } // namespace UnitTest