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.
337 lines
15 KiB
C++
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));
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
}
|