/* * 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)); } } } } }