You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Gems/Terrain/Code/Tests/ClipmapBoundsTests.cpp

337 lines
15 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzCore/UnitTest/TestTypes.h>
#include <gmock/gmock.h>
#include <TerrainRenderer/ClipmapBounds.h>
#include <TerrainRenderer/Aabb2i.h>
#include <AzCore/std/containers/span.h>
#include <AzCore/std/containers/vector.h>
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<int32_t>(AZStd::lround(desc.m_worldSpaceCenter.GetX() / desc.m_clipToWorldScale)),
aznumeric_cast<int32_t>(AZStd::lround(desc.m_worldSpaceCenter.GetY() / desc.m_clipToWorldScale))
};
localMax += aznumeric_cast<int32_t>(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<Terrain::ClipmapBoundsRegion> 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<float>(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));
}
}
}
}
}