From 453808eb908c9f45970cc42a87d30f3cad523173 Mon Sep 17 00:00:00 2001 From: Ken Pruiksma Date: Wed, 26 Jan 2022 14:30:13 -0600 Subject: [PATCH] Clipmap bounds class (#7134) * ClipmapBounds class - This class is built to keep track of textures for clipmap like structures where there is a virtual center point and the edges need to be updated as the camera moves around the world Signed-off-by: Ken Pruiksma * Removing dead code Signed-off-by: Ken Pruiksma * Updates from PR feedback. Signed-off-by: Ken Pruiksma * comment update Signed-off-by: Ken Pruiksma * Updates from review suggestions Signed-off-by: Ken Pruiksma * More updates. Moved the snapped center point calculation out to a separate function so the constructor doesn't need to do unnecessary work. Signed-off-by: Ken Pruiksma * Removing unused variable. Signed-off-by: Ken Pruiksma * Fixing bug in unit test that was doing a comparison and throwing away the result instead of actually testing it. Signed-off-by: Ken Pruiksma * Adding some comments and constifying some functions. Signed-off-by: Ken Pruiksma * Fixing numeric casting issue on linux Signed-off-by: Ken Pruiksma --- .../Code/Source/TerrainRenderer/Aabb2i.cpp | 32 +- .../Code/Source/TerrainRenderer/Aabb2i.h | 4 + .../Source/TerrainRenderer/ClipmapBounds.cpp | 278 +++++++++++++++ .../Source/TerrainRenderer/ClipmapBounds.h | 159 +++++++++ .../Code/Source/TerrainRenderer/Vector2i.cpp | 71 +++- .../Code/Source/TerrainRenderer/Vector2i.h | 14 +- .../Terrain/Code/Tests/ClipmapBoundsTests.cpp | 336 ++++++++++++++++++ Gems/Terrain/Code/terrain_files.cmake | 6 +- Gems/Terrain/Code/terrain_tests_files.cmake | 9 +- 9 files changed, 892 insertions(+), 17 deletions(-) create mode 100644 Gems/Terrain/Code/Source/TerrainRenderer/ClipmapBounds.cpp create mode 100644 Gems/Terrain/Code/Source/TerrainRenderer/ClipmapBounds.h create mode 100644 Gems/Terrain/Code/Tests/ClipmapBoundsTests.cpp diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/Aabb2i.cpp b/Gems/Terrain/Code/Source/TerrainRenderer/Aabb2i.cpp index c38651f296..82d5a6bf2d 100644 --- a/Gems/Terrain/Code/Source/TerrainRenderer/Aabb2i.cpp +++ b/Gems/Terrain/Code/Source/TerrainRenderer/Aabb2i.cpp @@ -18,12 +18,40 @@ namespace Terrain Aabb2i Aabb2i::operator+(const Vector2i& rhs) const { - return { m_min + rhs, m_max + rhs }; + Aabb2i returnValue = *this; + returnValue += rhs; + return returnValue; + } + + Aabb2i& Aabb2i::operator+=(const Vector2i& rhs) + { + m_min += rhs; + m_max += rhs; + return *this; } Aabb2i Aabb2i::operator-(const Vector2i& rhs) const { - return *this + -rhs; + Aabb2i returnValue = *this; + returnValue -= rhs; + return returnValue; + } + + Aabb2i& Aabb2i::operator-=(const Vector2i& rhs) + { + m_min -= rhs; + m_max -= rhs; + return *this; + } + + bool Aabb2i::operator==(const Aabb2i& other) const + { + return m_min == other.m_min && m_max == other.m_max; + } + + bool Aabb2i::operator!=(const Aabb2i& other) const + { + return !(*this == other); } Aabb2i Aabb2i::GetClamped(Aabb2i rhs) const diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/Aabb2i.h b/Gems/Terrain/Code/Source/TerrainRenderer/Aabb2i.h index 88f735564d..3d350c8c3a 100644 --- a/Gems/Terrain/Code/Source/TerrainRenderer/Aabb2i.h +++ b/Gems/Terrain/Code/Source/TerrainRenderer/Aabb2i.h @@ -21,7 +21,11 @@ namespace Terrain Aabb2i(const Vector2i& min, const Vector2i& max); Aabb2i operator+(const Vector2i& offset) const; + Aabb2i& operator+=(const Vector2i& offset); Aabb2i operator-(const Vector2i& offset) const; + Aabb2i& operator-=(const Vector2i& offset); + bool operator==(const Aabb2i& other) const; + bool operator!=(const Aabb2i& other) const; Aabb2i GetClamped(Aabb2i rhs) const; bool IsValid() const; diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/ClipmapBounds.cpp b/Gems/Terrain/Code/Source/TerrainRenderer/ClipmapBounds.cpp new file mode 100644 index 0000000000..29cc853328 --- /dev/null +++ b/Gems/Terrain/Code/Source/TerrainRenderer/ClipmapBounds.cpp @@ -0,0 +1,278 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include + +namespace Terrain +{ + bool ClipmapBoundsRegion::operator==(const ClipmapBoundsRegion& other) const + { + return m_localAabb == other.m_localAabb && m_worldAabb.IsClose(other.m_worldAabb); + } + + bool ClipmapBoundsRegion::operator!=(const ClipmapBoundsRegion& other) const + { + return !(*this == other); + } + + ClipmapBounds::ClipmapBounds(const ClipmapBoundsDescriptor& desc) + : m_size(desc.m_size) + , m_halfSize(desc.m_size >> 1) + , m_clipmapUpdateMultiple(AZ::GetMax(desc.m_clipmapUpdateMultiple, 1)) + , m_scale(desc.m_clipToWorldScale) + , m_rcpScale(1.0f / desc.m_clipToWorldScale) + { + AZ_Error("ClipmapBounds", m_scale > 0.0f, "ClipmapBounds should have a scale that is greater than 0.0f."); + m_scale = AZ::GetMax(m_scale, AZ::Constants::FloatEpsilon); + + // recalculate m_center + m_center = GetSnappedCenter(GetClipSpaceVector(desc.m_worldSpaceCenter)); + } + + auto ClipmapBounds::UpdateCenter(const AZ::Vector2& newCenter, AZ::Aabb* untouchedRegion) -> ClipmapBoundsRegionList + { + return UpdateCenter(GetClipSpaceVector(newCenter), untouchedRegion); + } + + auto ClipmapBounds::UpdateCenter(const Vector2i& newCenter, AZ::Aabb* untouchedRegion) -> ClipmapBoundsRegionList + { + AZStd::vector updateRegions; + + // If the new snapped center isn't the same as the old, then generate update regions in clipmap space + Vector2i updatedCenter = GetSnappedCenter(newCenter); + + int32_t xDiff = updatedCenter.m_x - m_center.m_x; + int32_t updateWidth = AZStd::GetMin(abs(xDiff), m_size); + + /* + Calculate the update regions. In the common case, there will be two update regions that form either + an L or inverted L shape. To avoid double-counting the corner, it is always put in the vertical box: + _ + | | + | |____ + |_|____| + + */ + + // Calculate the vertical box + if (updatedCenter.m_x != m_center.m_x) + { + updateRegions.push_back(); + Aabb2i& updateRegion = updateRegions.back(); + + if (updatedCenter.m_x < m_center.m_x) + { + updateRegion.m_min.m_x = updatedCenter.m_x - m_halfSize; + updateRegion.m_max.m_x = updateRegion.m_min.m_x + updateWidth; + } + else + { + updateRegion.m_max.m_x = updatedCenter.m_x + m_halfSize; + updateRegion.m_min.m_x = updateRegion.m_max.m_x - updateWidth; + } + updateRegion.m_min.m_y = updatedCenter.m_y - m_halfSize; + updateRegion.m_max.m_y = updatedCenter.m_y + m_halfSize; + } + + // Calculate the horizontal box + if (updatedCenter.m_y != m_center.m_y && updateWidth < m_size) + { + updateRegions.push_back(); + Aabb2i& updateRegion = updateRegions.back(); + + uint32_t updateHeight = AZStd::GetMin(abs(updatedCenter.m_y - m_center.m_y), m_size); + if (updatedCenter.m_y < m_center.m_y) + { + updateRegion.m_min.m_y = updatedCenter.m_y - m_halfSize; + updateRegion.m_max.m_y = updateRegion.m_min.m_y + updateHeight; + } + else + { + updateRegion.m_max.m_y = updatedCenter.m_y + m_halfSize; + updateRegion.m_min.m_y = updateRegion.m_max.m_y - updateHeight; + } + + // If there was a vertical box, then don't double-count the corner of the update. + if (xDiff < 0) + { + updateRegion.m_min.m_x = updatedCenter.m_x - m_halfSize + updateWidth; + updateRegion.m_max.m_x = updatedCenter.m_x + m_halfSize; + } + else if (xDiff > 0) + { + updateRegion.m_min.m_x = updatedCenter.m_x - m_halfSize; + updateRegion.m_max.m_x = updatedCenter.m_x + m_halfSize - updateWidth; + } + } + + if (untouchedRegion) + { + // Default to the entire area being untouched. + AZ::Aabb worldBounds = GetWorldBounds(); + float maxX = worldBounds.GetMax().GetX(); + float minX = worldBounds.GetMin().GetX(); + float maxY = worldBounds.GetMax().GetY(); + float minY = worldBounds.GetMin().GetY(); + + if (updatedCenter.m_x < m_center.m_x) + { + maxX = (updatedCenter.m_x + m_halfSize) * m_rcpScale; + } + else if (updatedCenter.m_x > m_center.m_x) + { + minX = (updatedCenter.m_x - m_halfSize) * m_rcpScale; + } + if (updatedCenter.m_y < m_center.m_y) + { + maxY = (updatedCenter.m_y + m_halfSize) * m_rcpScale; + } + else if (updatedCenter.m_y > m_center.m_y) + { + minY = (updatedCenter.m_y - m_halfSize) * m_rcpScale; + } + + untouchedRegion->Set(AZ::Vector3(minX, minY, 0.0f), AZ::Vector3(maxX, maxY, 0.0f)); + } + + m_center = updatedCenter; + + m_modCenter.m_x = (m_size + (m_center.m_x % m_size)) % m_size; + m_modCenter.m_y = (m_size + (m_center.m_y % m_size)) % m_size; + + ClipmapBoundsRegionList boundsUpdate; + for (Aabb2i& updateRegion : updateRegions) + { + ClipmapBoundsRegionList update = TransformRegion(updateRegion); + boundsUpdate.insert(boundsUpdate.end(), update.begin(), update.end()); + } + + return boundsUpdate; + } + + auto ClipmapBounds::TransformRegion(AZ::Aabb worldSpaceRegion) -> ClipmapBoundsRegionList + { + AZ::Vector2 worldMin = AZ::Vector2(worldSpaceRegion.GetMin().GetX(), worldSpaceRegion.GetMin().GetY()); + AZ::Vector2 worldMax = AZ::Vector2(worldSpaceRegion.GetMax().GetX(), worldSpaceRegion.GetMax().GetY()); + + Aabb2i clipSpaceRegion; + clipSpaceRegion.m_min = GetClipSpaceVector(worldMin); + clipSpaceRegion.m_max = GetClipSpaceVector(worldMax); + + return TransformRegion(clipSpaceRegion); + } + + auto ClipmapBounds::TransformRegion(Aabb2i region) -> ClipmapBoundsRegionList + { + ClipmapBoundsRegionList transformedRegions; + + Aabb2i clampedRegion = region.GetClamped(GetLocalBounds()); + if (!clampedRegion.IsValid()) + { + // Early out if the region is outside the bounds + return transformedRegions; + } + + Vector2i minCorner = m_center - m_halfSize; + + Vector2i minBoundary; + minBoundary.m_x = (minCorner.m_x / m_size - (minCorner.m_x < 0 ? 1 : 0)) * m_size; + minBoundary.m_y = (minCorner.m_y / m_size - (minCorner.m_y < 0 ? 1 : 0)) * m_size; + + Aabb2i bottomLeftTile = Aabb2i(minBoundary, minBoundary + m_size); + + // For each of the 4 quadrants: + auto calculateQuadrant = [&](Aabb2i tile) + { + Aabb2i regionClampedToTile = clampedRegion.GetClamped(tile); + if (regionClampedToTile.IsValid()) + { + transformedRegions.push_back( + ClipmapBoundsRegion({ + GetWorldSpaceAabb(regionClampedToTile), + regionClampedToTile - tile.m_min + }) + ); + } + }; + + calculateQuadrant(bottomLeftTile); + calculateQuadrant(bottomLeftTile + Vector2i(m_size, 0)); + calculateQuadrant(bottomLeftTile + Vector2i(0, m_size)); + calculateQuadrant(bottomLeftTile + Vector2i(m_size, m_size)); + + return transformedRegions; + } + + AZ::Aabb ClipmapBounds::GetWorldBounds() const + { + Aabb2i localBounds = GetLocalBounds(); + + return AZ::Aabb::CreateFromMinMaxValues( + localBounds.m_min.m_x * m_scale, localBounds.m_min.m_y * m_scale, 0.0f, + localBounds.m_max.m_x * m_scale, localBounds.m_max.m_y * m_scale, 0.0f); + } + + float ClipmapBounds::GetWorldSpaceSafeDistance() const + { + return (m_halfSize - m_clipmapUpdateMultiple) * m_scale; + } + + Vector2i ClipmapBounds::GetSnappedCenter(const Vector2i& center) + { + Vector2i updatedCenter = m_center; + + // Update the snapped center if the new center has drifted beyond the margin + auto UpdateDim = [&](int32_t centerDim, int32_t& snappedCenterDim) -> void + { + int32_t diff = centerDim - snappedCenterDim; + + int32_t scaledCenterDim = (centerDim / m_clipmapUpdateMultiple); + if (centerDim < 0) + { + // Force rounding down for negatives + scaledCenterDim--; + } + + if (diff >= m_clipmapUpdateMultiple) + { + snappedCenterDim = scaledCenterDim * m_clipmapUpdateMultiple; + } + if (diff < -m_clipmapUpdateMultiple) + { + snappedCenterDim = (scaledCenterDim + 1) * m_clipmapUpdateMultiple; + } + }; + UpdateDim(center.m_x, updatedCenter.m_x); + UpdateDim(center.m_y, updatedCenter.m_y); + + return updatedCenter; + } + + Aabb2i ClipmapBounds::GetLocalBounds() const + { + return Aabb2i(m_center - m_halfSize, m_center + m_halfSize); + } + + Vector2i ClipmapBounds::GetClipSpaceVector(const AZ::Vector2& worldSpaceVector) const + { + // Get rounded integer x/y coords in clipmap space. + int32_t x = AZStd::lround(worldSpaceVector.GetX() * m_rcpScale); + int32_t y = AZStd::lround(worldSpaceVector.GetY() * m_rcpScale); + return Vector2i(x, y); + } + + AZ::Aabb ClipmapBounds::GetWorldSpaceAabb(const Aabb2i& clipSpaceAabb) const + { + return AZ::Aabb::CreateFromMinMaxValues( + clipSpaceAabb.m_min.m_x * m_scale, clipSpaceAabb.m_min.m_y * m_scale, 0.0f, + clipSpaceAabb.m_max.m_x * m_scale, clipSpaceAabb.m_max.m_y * m_scale, 0.0f + ); + } +} diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/ClipmapBounds.h b/Gems/Terrain/Code/Source/TerrainRenderer/ClipmapBounds.h new file mode 100644 index 0000000000..1aac4d8c9e --- /dev/null +++ b/Gems/Terrain/Code/Source/TerrainRenderer/ClipmapBounds.h @@ -0,0 +1,159 @@ +/* + * 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 +#include +#include +#include + +namespace Terrain +{ + + struct ClipmapBoundsDescriptor + { + //! Width and height of the clipmap in texels. + uint32_t m_size = 1024; + + //! Current center location of the clipmap in world space + AZ::Vector2 m_worldSpaceCenter = AZ::Vector2::CreateZero(); + + // Updates to the clipmap will be produced in multiples of this value. This + // allows for larger but less frequent updates, and gives some wiggle room + // for each movement before an update is triggered. + // Note: This also means that whatever uses this clipmap should only ever + // display m_size - (2 * m_clipmapUpdateMultiple) pixels from the clipmap. + // Use GetWorldSpaceSafeDistance() to get the safe distance from center. + uint32_t m_clipmapUpdateMultiple = 4; + + //! Scale of the clip map compared to the world. A scale of 0.5 means that + //! a clipmap of size 1024 would cover 512 meters. + float m_clipToWorldScale = 1.0f; + }; + + struct ClipmapBoundsRegion + { + //! The world bounds of the updated region. Z is ignored. + AZ::Aabb m_worldAabb; + + //! The clipmaps bounds of the updated region. Will always be between 0 and size. + //! Min inclusive, max exclusive. + Aabb2i m_localAabb; + + bool operator==(const ClipmapBoundsRegion& other) const; + bool operator!=(const ClipmapBoundsRegion& other) const; + }; + + // This class manages a single clipmap region. A clipmap is a virtual view into a much larger + // region, where the clipmap view is centered around a point like the current camera position. + // The clipmap texture wraps to form a repeating grid and never moves, but only data within + // the clipmap bounds is actually valid. This makes looking up data in the clipmap trivial + // since it's just the world coordinate scaled by some amount. This technique also allows for + // only the edge areas of the clipmap to be updated as the center point moves around the world. + // + // The edges of the clipmap bounds will typically run through the texture, dividing it into 4 + // regions, except in cases where the clipmap bounds happen to be aligned with the underlying + // grid. This means whenever some bounding box needs to be updated in the clipmap, it may actually + // translate to 4 different areas of the underlying texture - one for each quadrant. + // + // This class aids in figuring out which areas of a clipmap need to be updated as its center point + // moves around in the world, and can map a single region that needs to be updated into several + // separate regions for each quadrant. + + /* + ___________________________ + | | | | | Clipmap Clipmap + | | | | | Bounds Texture (Tiled) + |______|______|______|______| ______ ______ + | | _|____ | | | | | |____|_| + | | | | | | | |_|_*__| | | | + |______|____|_|_*__|_|______| |_|____| |_*__|_| + | | |_|____| | | + | | | | | + |______|______|______|______| + | | | | | + | | | | | + |______|______|______|______| + + */ + + class ClipmapBounds + { + public: + + explicit ClipmapBounds(const ClipmapBoundsDescriptor& desc); + ~ClipmapBounds() = default; + + using ClipmapBoundsRegionList = AZStd::vector; + + //! Updates the clipmap bounds using a world coordinate center position and returns + //! 0-2 regions that need to be updated due to moving beyond the margins. These update + //! regions will always be at least the size of the margin, and will represent horizontal + //! and/or vertical strips along the edges of the clipmap. An optional untouched region + //! aabb can be passed to this function to get an aabb of areas inside the bounds of the + //! clipmap but not updated by the center moving. This can be useful in cases where part + //! of the bounds of the clipmap is dirty, but areas that will already be updated due + //! to the center moving shouldn't be updated twice. + ClipmapBoundsRegionList UpdateCenter(const AZ::Vector2& newCenter, AZ::Aabb* untouchedRegion = nullptr); + + //! Updates the clipmap bounds using a position in clipmap space (no scaling) and returns + //! 0-2 regions that need to be updated due to moving beyond the margins. These update + //! regions will always be at least the size of the margin, and will represent horizontal + //! and/or vertical strips along the edges of the clipmap. An optional untouched region + //! aabb can be passed to this function to get an aabb of areas inside the bounds of the + //! clipmap but not updated by the center moving. This can be useful in cases where part + //! of the bounds of the clipmap is dirty, but areas that will already be updated due + //! to the center moving shouldn't be updated twice. + ClipmapBoundsRegionList UpdateCenter(const Vector2i& newCenter, AZ::Aabb* untouchedRegion = nullptr); + + //! Takes in a single world space region and transforms it into 0-4 regions in the clipmap clamped + //! to the bounds of the clipmap. + ClipmapBoundsRegionList TransformRegion(AZ::Aabb worldSpaceRegion); + + //! Takes in a single unscaled clipmap space region and transforms it into 0-4 regions in the clipmap clamped + //! to the bounds of the clipmap. + ClipmapBoundsRegionList TransformRegion(Aabb2i clipSpaceRegion); + + //! Returns the bounds covered by this clipmap in world space. Z component is always 0. + AZ::Aabb GetWorldBounds() const; + + //! Returns the safe x and y distance from the center in world space. This is based on the scale, + //! clipmap size, and m_clipmapUpdateMultiple. For example, a clipmap size 1024 with scale + //! 0.25 and margin of 4 would have a safe distance of (1024 * 0.5 - 4) * 0.25 = 127.0f. + float GetWorldSpaceSafeDistance() const; + + private: + + //! Returns the center point snapped to a multiple of m_clipmapUpdateMultiple. This isn't + //! a simple rounding operation. The value returned will only be different from the curernt + //! center if the value passed in is greater than m_clipmapUpdateMultiple away from the center. + Vector2i GetSnappedCenter(const Vector2i& center); + + //! Returns the bounds covered by the clipmap in local space + Aabb2i GetLocalBounds() const; + + //! Applies scale and averages a world space vector to get a clip space vector. + Vector2i GetClipSpaceVector(const AZ::Vector2& worldSpaceVector) const; + + //! Applies inverse scale to get a world aabb from clip space aabb. + AZ::Aabb GetWorldSpaceAabb(const Aabb2i& clipSpaceAabb) const; + + Vector2i m_center; + Vector2i m_modCenter; + int32_t m_size; + int32_t m_halfSize; + int32_t m_clipmapUpdateMultiple; + float m_scale; + float m_rcpScale; + + }; + +} diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/Vector2i.cpp b/Gems/Terrain/Code/Source/TerrainRenderer/Vector2i.cpp index 1df945b88c..058b9ae76f 100644 --- a/Gems/Terrain/Code/Source/TerrainRenderer/Vector2i.cpp +++ b/Gems/Terrain/Code/Source/TerrainRenderer/Vector2i.cpp @@ -7,35 +7,90 @@ */ #include +#include namespace Terrain { - auto Vector2i::operator+(const Vector2i& rhs) const -> Vector2i + + Vector2i::Vector2i(int32_t x, int32_t y) + : m_x(x) + , m_y(y) + {} + + Vector2i::Vector2i(uint32_t value) + : m_x(aznumeric_cast(value)) + , m_y(aznumeric_cast(value)) + {} + + Vector2i::Vector2i(int32_t value) + : m_x(value) + , m_y(value) + {} + + Vector2i Vector2i::operator+(const Vector2i& rhs) const { - Vector2i offsetPoint = *this; - offsetPoint += rhs; - return offsetPoint; + Vector2i returnPoint = *this; + returnPoint += rhs; + return returnPoint; } - auto Vector2i::operator+=(const Vector2i& rhs) -> Vector2i& + Vector2i& Vector2i::operator+=(const Vector2i& rhs) { m_x += rhs.m_x; m_y += rhs.m_y; return *this; } - auto Vector2i::operator-(const Vector2i& rhs) const -> Vector2i + Vector2i Vector2i::operator-(const Vector2i& rhs) const { return *this + -rhs; } - auto Vector2i::operator-=(const Vector2i& rhs) -> Vector2i& + Vector2i& Vector2i::operator-=(const Vector2i& rhs) { return *this += -rhs; } - auto Vector2i::operator-() const -> Vector2i + Vector2i Vector2i::operator-() const { return {-m_x, -m_y}; } + + Vector2i Vector2i::operator*(const Vector2i& rhs) const + { + Vector2i returnPoint = *this; + returnPoint *= rhs; + return returnPoint; + } + + Vector2i& Vector2i::operator*=(const Vector2i& rhs) + { + m_x *= rhs.m_x; + m_y *= rhs.m_y; + return *this; + } + + Vector2i Vector2i::operator/(const Vector2i& rhs) const + { + Vector2i returnPoint = *this; + returnPoint /= rhs; + return returnPoint; + } + + Vector2i& Vector2i::operator/=(const Vector2i& rhs) + { + m_x /= rhs.m_x; + m_y /= rhs.m_y; + return *this; + } + + bool Vector2i::operator==(const Vector2i& rhs) const + { + return rhs.m_x == m_x && rhs.m_y == m_y; + } + + bool Vector2i::operator!=(const Vector2i& rhs) const + { + return !(*this == rhs); + } } diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/Vector2i.h b/Gems/Terrain/Code/Source/TerrainRenderer/Vector2i.h index 1244972fe9..a7c598a91f 100644 --- a/Gems/Terrain/Code/Source/TerrainRenderer/Vector2i.h +++ b/Gems/Terrain/Code/Source/TerrainRenderer/Vector2i.h @@ -16,14 +16,26 @@ namespace Terrain { public: + Vector2i() = default; + Vector2i(int32_t x, int32_t y); + Vector2i(uint32_t value); + Vector2i(int32_t value); + Vector2i operator+(const Vector2i& rhs) const; Vector2i& operator+=(const Vector2i& rhs); Vector2i operator-(const Vector2i& rhs) const; Vector2i& operator-=(const Vector2i& rhs); Vector2i operator-() const; + Vector2i operator*(const Vector2i& rhs) const; + Vector2i& operator*=(const Vector2i& rhs); + Vector2i operator/(const Vector2i& rhs) const; + Vector2i& operator/=(const Vector2i& rhs); + + bool operator==(const Vector2i& rhs) const; + bool operator!=(const Vector2i& rhs) const; + int32_t m_x{ 0 }; int32_t m_y{ 0 }; - }; } diff --git a/Gems/Terrain/Code/Tests/ClipmapBoundsTests.cpp b/Gems/Terrain/Code/Tests/ClipmapBoundsTests.cpp new file mode 100644 index 0000000000..9b09deb8f5 --- /dev/null +++ b/Gems/Terrain/Code/Tests/ClipmapBoundsTests.cpp @@ -0,0 +1,336 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include + +#include +#include + +#include +#include + +namespace UnitTest +{ + class ClipmapBoundsTests + : public UnitTest::AllocatorsTestFixture + { + public: + void CheckTransformRegionFullBounds(const Terrain::ClipmapBoundsDescriptor& desc); + }; + + void ClipmapBoundsTests::CheckTransformRegionFullBounds(const Terrain::ClipmapBoundsDescriptor& desc) + { + Terrain::ClipmapBounds bounds(desc); + + AZ::Aabb worldBounds = bounds.GetWorldBounds(); + float worldBoundsSize = worldBounds.GetXExtent(); + + auto output = bounds.TransformRegion(worldBounds); + ASSERT_EQ(output.size(), 4); + + AZ::Vector2 boundary = AZ::Vector2( + floorf(worldBounds.GetMax().GetX() / worldBoundsSize), + floorf(worldBounds.GetMax().GetY() / worldBoundsSize) + ) * worldBoundsSize; + + Terrain::Vector2i localMax = { + aznumeric_cast(AZStd::lround(desc.m_worldSpaceCenter.GetX() / desc.m_clipToWorldScale)), + aznumeric_cast(AZStd::lround(desc.m_worldSpaceCenter.GetY() / desc.m_clipToWorldScale)) + }; + localMax += aznumeric_cast(desc.m_size / 2ul); + + int32_t intSize = int32_t(desc.m_size); + Terrain::Vector2i localBoundary = { + ((localMax.m_x % intSize) + intSize) % intSize, + ((localMax.m_y % intSize) + intSize) % intSize + }; + + // Check each quadrant returned + AZStd::vector expected; + expected.resize(4); + + expected.at(0).m_localAabb = Terrain::Aabb2i({localBoundary.m_x, localBoundary.m_y}, {intSize, intSize}); + expected.at(0).m_worldAabb = AZ::Aabb::CreateFromMinMaxValues( + worldBounds.GetMin().GetX(), worldBounds.GetMin().GetY(), 0.0f, + boundary.GetX(), boundary.GetY(), 0.0f); + + expected.at(1).m_localAabb = Terrain::Aabb2i({0, localBoundary.m_y}, {localBoundary.m_x, intSize}); + expected.at(1).m_worldAabb = AZ::Aabb::CreateFromMinMaxValues( + boundary.GetX(), worldBounds.GetMin().GetY(), 0.0f, + worldBounds.GetMax().GetX(), boundary.GetY(), 0.0f); + + expected.at(2).m_localAabb = Terrain::Aabb2i({localBoundary.m_x, 0}, {intSize, localBoundary.m_y}); + expected.at(2).m_worldAabb = AZ::Aabb::CreateFromMinMaxValues( + worldBounds.GetMin().GetX(), boundary.GetY(), 0.0f, + boundary.GetX(), worldBounds.GetMax().GetY(), 0.0f); + + expected.at(3).m_localAabb = Terrain::Aabb2i({ 0, 0 }, { localBoundary.m_x, localBoundary.m_y }); + expected.at(3).m_worldAabb = AZ::Aabb::CreateFromMinMaxValues( + boundary.GetX(), boundary.GetY(), 0.0f, + worldBounds.GetMax().GetX(), worldBounds.GetMax().GetY(), 0.0f); + + EXPECT_THAT(output, ::testing::UnorderedElementsAreArray(expected)); + } + + TEST_F(ClipmapBoundsTests, Construction) + { + Terrain::ClipmapBoundsDescriptor desc; + Terrain::ClipmapBounds bounds(desc); + } + + TEST_F(ClipmapBoundsTests, BasicTransform) + { + // Create clipmap around 0.0, so it's perfectly divided into 4 quadrants + Terrain::ClipmapBoundsDescriptor desc; + desc.m_worldSpaceCenter = AZ::Vector2(0.0f, 0.0f); + desc.m_clipmapUpdateMultiple = 0; + desc.m_clipToWorldScale = 1.0f; + desc.m_size = 1024; + Terrain::ClipmapBounds bounds(desc); + + auto output = bounds.TransformRegion(AZ::Aabb::CreateFromMinMaxValues(-512.0f, -512.0f, 0.0f, 512.0f, 512.0f, 0.0f)); + + ASSERT_EQ(output.size(), 4); + + // Check each quadrant returned + EXPECT_EQ(output.at(0).m_localAabb, Terrain::Aabb2i({512, 512}, {1024, 1024})); + EXPECT_TRUE(output.at(0).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(-512.0f, -512.0f, 0.0f, 0.0f, 0.0f, 0.0f))); + EXPECT_EQ(output.at(1).m_localAabb, Terrain::Aabb2i({0, 512}, {512, 1024})); + EXPECT_TRUE(output.at(1).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(0.0f, -512.0f, 0.0f, 512.0f, 0.0f, 0.0f))); + EXPECT_EQ(output.at(2).m_localAabb, Terrain::Aabb2i({512, 0}, {1024, 512})); + EXPECT_TRUE(output.at(2).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(-512.0f, 0.0f, 0.0f, 0.0f, 512.0f, 0.0f))); + EXPECT_EQ(output.at(3).m_localAabb, Terrain::Aabb2i({0, 0}, {512, 512})); + EXPECT_TRUE(output.at(3).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 0.0f, 512.0f, 512.0f, 0.0f))); + } + + TEST_F(ClipmapBoundsTests, ScaledTransform) + { + // Create clipmap around 0.0, so it's perfectly divided into 4 quadrants, but half-scale + Terrain::ClipmapBoundsDescriptor desc; + desc.m_worldSpaceCenter = AZ::Vector2(0.0f, 0.0f); + desc.m_clipmapUpdateMultiple = 0; + desc.m_clipToWorldScale = 0.5f; + desc.m_size = 1024; + Terrain::ClipmapBounds bounds(desc); + + auto output = bounds.TransformRegion(AZ::Aabb::CreateFromMinMaxValues(-256.0f, -256.0f, 0.0f, 256.0f, 256.0f, 0.0f)); + + ASSERT_EQ(output.size(), 4); + + // Check each quadrant returned + EXPECT_EQ(output.at(0).m_localAabb, Terrain::Aabb2i({512, 512}, {1024, 1024})); + EXPECT_TRUE(output.at(0).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(-256.0f, -256.0f, 0.0f, 0.0f, 0.0f, 0.0f))); + EXPECT_EQ(output.at(1).m_localAabb, Terrain::Aabb2i({0, 512}, {512, 1024})); + EXPECT_TRUE(output.at(1).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(0.0f, -256.0f, 0.0f, 256.0f, 0.0f, 0.0f))); + EXPECT_EQ(output.at(2).m_localAabb, Terrain::Aabb2i({512, 0}, {1024, 512})); + EXPECT_TRUE(output.at(2).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(-256.0f, 0.0f, 0.0f, 0.0f, 256.0f, 0.0f))); + EXPECT_EQ(output.at(3).m_localAabb, Terrain::Aabb2i({0, 0}, {512, 512})); + EXPECT_TRUE(output.at(3).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 0.0f, 256.0f, 256.0f, 0.0f))); + } + + TEST_F(ClipmapBoundsTests, ComplexTransformsFullBounds) + { + // Check 4 different clipmaps - one in completely positive space, one in negative space, and two straddling the axis + + // Clipmap in negative space + { + Terrain::ClipmapBoundsDescriptor desc; + desc.m_worldSpaceCenter = AZ::Vector2(-1234.0f, -5432.0f); + desc.m_clipmapUpdateMultiple = 0; + desc.m_clipToWorldScale = 0.75f; + desc.m_size = 512; + CheckTransformRegionFullBounds(desc); + } + + // Clipmap in positive space + { + Terrain::ClipmapBoundsDescriptor desc; + desc.m_worldSpaceCenter = AZ::Vector2(1234.0f, 5432.0f); + desc.m_clipmapUpdateMultiple = 0; + desc.m_clipToWorldScale = 1.25f; + desc.m_size = 1024; + CheckTransformRegionFullBounds(desc); + } + + // Clipmap on x axis + { + Terrain::ClipmapBoundsDescriptor desc; + desc.m_worldSpaceCenter = AZ::Vector2(1234.0f, -100.0f); + desc.m_clipmapUpdateMultiple = 0; + desc.m_clipToWorldScale = 1.5f; + desc.m_size = 256; + CheckTransformRegionFullBounds(desc); + } + // Clipmap on y axis + { + Terrain::ClipmapBoundsDescriptor desc; + desc.m_worldSpaceCenter = AZ::Vector2(-100.0f, 5432.0f); + desc.m_clipmapUpdateMultiple = 0; + desc.m_clipToWorldScale = 1.0f; + desc.m_size = 2048; + CheckTransformRegionFullBounds(desc); + } + } + + TEST_F(ClipmapBoundsTests, TransformSmallBounds) + { + // Create clipmap around 0.0, so it's perfectly divided into 4 quadrants + Terrain::ClipmapBoundsDescriptor desc; + desc.m_worldSpaceCenter = AZ::Vector2(0.0f, 0.0f); + desc.m_clipmapUpdateMultiple = 0; + desc.m_clipToWorldScale = 1.0f; + desc.m_size = 1024; + Terrain::ClipmapBounds bounds(desc); + + { + // Single quadrant positive + + AZ::Aabb smallArea = AZ::Aabb::CreateFromMinMaxValues( + 10.0f, 10.0f, 0.0f, 50.0f, 50.0f, 0.0f + ); + + auto output = bounds.TransformRegion(smallArea); + + ASSERT_EQ(output.size(), 1); + EXPECT_EQ(output.at(0).m_localAabb, Terrain::Aabb2i({10, 10}, {50, 50})); + EXPECT_TRUE(output.at(0).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(10.0f, 10.0f, 0.0f, 50.0f, 50.0f, 0.0f))); + } + + { + // Single quadrant negative + + AZ::Aabb smallArea = AZ::Aabb::CreateFromMinMaxValues( + -50.0f, -50.0f, 0.0f, -10.0f, -10.0f, 0.0f + ); + + auto output = bounds.TransformRegion(smallArea); + + ASSERT_EQ(output.size(), 1); + EXPECT_EQ(output.at(0).m_localAabb, Terrain::Aabb2i({974, 974}, {1014, 1014})); + EXPECT_TRUE(output.at(0).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(-50.0f, -50.0f, 0.0f, -10.0f, -10.0f, 0.0f))); + } + + { + // 2 quadrant positive + + AZ::Aabb smallArea = AZ::Aabb::CreateFromMinMaxValues( + 10.0f, -10.0f, 0.0f, 50.0f, 50.0f, 0.0f + ); + + auto output = bounds.TransformRegion(smallArea); + + ASSERT_EQ(output.size(), 2); + EXPECT_EQ(output.at(0).m_localAabb, Terrain::Aabb2i({10, 1014}, {50, 1024})); + EXPECT_TRUE(output.at(0).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(10.0f, -10.0f, 0.0f, 50.0f, 0.0f, 0.0f))); + EXPECT_EQ(output.at(1).m_localAabb, Terrain::Aabb2i({10, 0}, {50, 50})); + EXPECT_TRUE(output.at(1).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(10.0f, 0.0f, 0.0f, 50.0f, 50.0f, 0.0f))); + } + } + + TEST_F(ClipmapBoundsTests, MarginReducesUpdates) + { + // With a margin defined, the bounds should only trigger updates when the camera moves outside the margins + + // Create clipmap around 0.0, so it's perfectly divided into 4 quadrants + Terrain::ClipmapBoundsDescriptor desc; + desc.m_worldSpaceCenter = AZ::Vector2(0.0f, 0.0f); + desc.m_clipmapUpdateMultiple = 16; + desc.m_clipToWorldScale = 1.0f; + desc.m_size = 1024; + Terrain::ClipmapBounds bounds(desc); + + // center moved forward to 10, still within margin + auto output1 = bounds.UpdateCenter(AZ::Vector2(10.0f, 10.0f)); + EXPECT_EQ(output1.size(), 0); + // center moved forwrd to 20, beyond margin, triggers update + auto output2 = bounds.UpdateCenter(AZ::Vector2(20.0f, 20.0f)); + EXPECT_GT(output2.size(), 0); + // center moved back to 10, still within margin + auto output3 = bounds.UpdateCenter(AZ::Vector2(10.0f, 10.0f)); + EXPECT_EQ(output3.size(), 0); + // center moved back to 0, still within margin (on edge) + auto output4 = bounds.UpdateCenter(AZ::Vector2(0.0f, 0.0f)); + EXPECT_EQ(output4.size(), 0); + // center moved back to -10, beyond margin, triggers update + auto output5 = bounds.UpdateCenter(AZ::Vector2(-10.0f, -10.0f)); + EXPECT_GT(output5.size(), 0); + } + + TEST_F(ClipmapBoundsTests, CenterMovementUpdates) + { + // Create clipmap around 0.0, so it's perfectly divided into 4 quadrants + Terrain::ClipmapBoundsDescriptor desc; + desc.m_worldSpaceCenter = AZ::Vector2(0.0f, 0.0f); + desc.m_clipmapUpdateMultiple = 16; + desc.m_clipToWorldScale = 1.0f; + desc.m_size = 1024; + Terrain::ClipmapBounds bounds(desc); + + { + AZ::Aabb untouchedRegion = AZ::Aabb::CreateNull(); + auto output = bounds.UpdateCenter(AZ::Vector2(20.0f, 20.0f), &untouchedRegion); + ASSERT_EQ(output.size(), 4); + + // Instead of checking bounds directly, do several checks to make sure the bounds are appropriate. Since + // the center moved just outside the margin along the diagonal, we should expect two edges to be updated + // that are the width of the margin. + + // 1. The number of pixels updated in the bounds should be two sides of margin width + float pixelsCovered = 0; + for (auto& region : output) + { + // Note: GetSurfaceArea() returns the area of all 6 sides of the aabb. With a Z extent of 0, that + // means that only the top and bottom will be counted, so we need to multiply by 0.5. + pixelsCovered += region.m_worldAabb.GetSurfaceArea() * 0.5f; + } + + // Two edges of margin * size, minus the overlap in the corner. + const uint32_t updateMultiple = desc.m_clipmapUpdateMultiple; + float expectedCoverage = updateMultiple * desc.m_size * 2.0f - updateMultiple * updateMultiple; + EXPECT_NEAR(pixelsCovered, expectedCoverage, 0.0001f); + + // 2. The untouched region area should match what's expected + float untouchedRegionArea = untouchedRegion.GetSurfaceArea() * 0.5f; + float expectedUntouchedRegionSide = aznumeric_cast(desc.m_size - desc.m_clipmapUpdateMultiple); + float expectedUntouchedRegionArea = expectedUntouchedRegionSide * expectedUntouchedRegionSide; + EXPECT_NEAR(untouchedRegionArea, expectedUntouchedRegionArea, 0.0001f); + + // 3. All of the update regions should be inside the world bounds of the clipmap + AZ::Aabb worldBounds = bounds.GetWorldBounds(); + for (auto& region : output) + { + EXPECT_EQ(region.m_worldAabb.GetClamped(worldBounds), region.m_worldAabb); + } + + // 4. The untouched region should also be inside the world bounds of the clipmap; + EXPECT_EQ(untouchedRegion.GetClamped(worldBounds), untouchedRegion); + + // 5. None of the update regions should overlap each other or the untouched region + + // push the untouched region on the vector to make comparisons easier + output.push_back(Terrain::ClipmapBoundsRegion({untouchedRegion, Terrain::Aabb2i({}) })); + for (uint32_t i = 0; i < output.size(); ++i) + { + const AZ::Aabb boundsToCheck = output.at(i).m_worldAabb; + for (uint32_t j = i + 1; j < output.size(); ++j) + { + // AZ::Aabb::Overlaps() counts touching edges as overlapping, so we need a strict version + auto strictOverlaps = [](const AZ::Aabb& aabb1, const AZ::Aabb& aabb2) -> bool + { + return aabb1.GetMin().IsLessThan(aabb2.GetMax()) && + aabb1.GetMax().IsGreaterThan(aabb2.GetMin()); + }; + EXPECT_FALSE(strictOverlaps(boundsToCheck, output.at(j).m_worldAabb)); + } + } + + } + + } +} diff --git a/Gems/Terrain/Code/terrain_files.cmake b/Gems/Terrain/Code/terrain_files.cmake index 4b994c2587..53a7c6ac6d 100644 --- a/Gems/Terrain/Code/terrain_files.cmake +++ b/Gems/Terrain/Code/terrain_files.cmake @@ -35,6 +35,10 @@ set(FILES Source/TerrainRenderer/Components/TerrainMacroMaterialComponent.h Source/TerrainRenderer/Aabb2i.cpp Source/TerrainRenderer/Aabb2i.h + Source/TerrainRenderer/BindlessImageArrayHandler.cpp + Source/TerrainRenderer/BindlessImageArrayHandler.h + Source/TerrainRenderer/ClipmapBounds.cpp + Source/TerrainRenderer/ClipmapBounds.h Source/TerrainRenderer/TerrainFeatureProcessor.cpp Source/TerrainRenderer/TerrainFeatureProcessor.h Source/TerrainRenderer/TerrainDetailMaterialManager.cpp @@ -43,8 +47,6 @@ set(FILES Source/TerrainRenderer/TerrainMacroMaterialManager.h Source/TerrainRenderer/TerrainMeshManager.cpp Source/TerrainRenderer/TerrainMeshManager.h - Source/TerrainRenderer/BindlessImageArrayHandler.cpp - Source/TerrainRenderer/BindlessImageArrayHandler.h Source/TerrainRenderer/TerrainAreaMaterialRequestBus.h Source/TerrainRenderer/TerrainMacroMaterialBus.cpp Source/TerrainRenderer/TerrainMacroMaterialBus.h diff --git a/Gems/Terrain/Code/terrain_tests_files.cmake b/Gems/Terrain/Code/terrain_tests_files.cmake index 1a46fdd203..39cbe0861b 100644 --- a/Gems/Terrain/Code/terrain_tests_files.cmake +++ b/Gems/Terrain/Code/terrain_tests_files.cmake @@ -7,14 +7,15 @@ # set(FILES - Tests/TerrainTest.cpp - Tests/TerrainSystemTest.cpp + Tests/ClipmapBoundsTests.cpp Tests/LayerSpawnerTests.cpp - Tests/TerrainPhysicsColliderTests.cpp - Tests/SurfaceMaterialsListTest.cpp Tests/MockAxisAlignedBoxShapeComponent.h Tests/TerrainHeightGradientListTests.cpp Tests/TerrainMacroMaterialTests.cpp + Tests/SurfaceMaterialsListTest.cpp + Tests/TerrainPhysicsColliderTests.cpp Tests/TerrainSurfaceGradientListTests.cpp Tests/TerrainSystemBenchmarks.cpp + Tests/TerrainSystemTest.cpp + Tests/TerrainTest.cpp )