/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ #include "GradientSignal_precompiled.h" #include #include #include #include "Tests/GradientSignalTestMocks.h" #include #include #include #include #include #include #include #include #include #include namespace UnitTest { struct GradientSignalReferencesTestsFixture : public GradientSignalTest { void TestMixedGradientComponent(int dataSize, const AZStd::vector& layer1Data, const AZStd::vector& layer2Data, const AZStd::vector& expectedOutput, GradientSignal::MixedGradientLayer::MixingOperation operation, float opacity) { auto mockLayer1 = CreateEntity(); const AZ::EntityId id1 = mockLayer1->GetId(); MockGradientArrayRequestsBus mockLayer1GradientRequestsBus(id1, layer1Data, dataSize); auto mockLayer2 = CreateEntity(); const AZ::EntityId id2 = mockLayer2->GetId(); MockGradientArrayRequestsBus mockLayer2GradientRequestsBus(id2, layer2Data, dataSize); GradientSignal::MixedGradientConfig config; GradientSignal::MixedGradientLayer layer; layer.m_enabled = true; layer.m_operation = GradientSignal::MixedGradientLayer::MixingOperation::Initialize; layer.m_gradientSampler.m_gradientId = mockLayer1->GetId(); layer.m_gradientSampler.m_opacity = 1.0f; config.m_layers.push_back(layer); layer.m_operation = operation; layer.m_gradientSampler.m_gradientId = mockLayer2->GetId(); layer.m_gradientSampler.m_opacity = opacity; config.m_layers.push_back(layer); auto entity = CreateEntity(); CreateComponent(entity.get(), config); ActivateEntity(entity.get()); TestFixedDataSampler(expectedOutput, dataSize, entity->GetId()); } void TestSurfaceSlopeGradientComponent(int dataSize, const AZStd::vector& inputAngles, const AZStd::vector& expectedOutput, float slopeMin, float slopeMax, GradientSignal::SurfaceSlopeGradientConfig::RampType rampType, float falloffMidpoint, float falloffRange, float falloffStrength) { MockSurfaceDataSystem mockSurfaceDataSystem; SurfaceData::SurfacePoint point; // Fill our mock surface with the correct normal value for each point based on our test angle set. for (int y = 0; y < dataSize; y++) { for (int x = 0; x < dataSize; x++) { float angle = AZ::DegToRad(inputAngles[(y * dataSize) + x]); point.m_normal = AZ::Vector3(sinf(angle), 0.0f, cosf(angle)); mockSurfaceDataSystem.m_GetSurfacePoints[AZStd::make_pair(static_cast(x), static_cast(y))] = { { point } }; } } GradientSignal::SurfaceSlopeGradientConfig config; config.m_slopeMin = slopeMin; config.m_slopeMax = slopeMax; config.m_rampType = rampType; config.m_smoothStep.m_falloffMidpoint = falloffMidpoint; config.m_smoothStep.m_falloffRange = falloffRange; config.m_smoothStep.m_falloffStrength = falloffStrength; auto entity = CreateEntity(); CreateComponent(entity.get(), config); ActivateEntity(entity.get()); TestFixedDataSampler(expectedOutput, dataSize, entity->GetId()); } }; TEST_F(GradientSignalReferencesTestsFixture, MixedGradientComponent_OperationInitialize) { // Mixed Gradient: Create two layers and set the second one to blend with "Initialize" with an opacity of 0.5f. // The output should exactly match the second layer at an opacity of 0.5f. (i.e. doesn't blend with layer 1, just overwrites) constexpr int dataSize = 3; AZStd::vector inputLayer1 = { 0.0f, 0.1f, 0.2f, 0.4f, 0.5f, 0.6f, 0.8f, 0.9f, 1.0f }; AZStd::vector inputLayer2 = { 0.06f, 0.16f, 0.26f, 0.46f, 0.56f, 0.66f, 0.86f, 0.94f, 0.96f }; // These values should be layer 2 * 0.5f, with no influence from layer 1. AZStd::vector expectedOutput = { 0.03f, 0.08f, 0.13f, 0.23f, 0.28f, 0.33f, 0.43f, 0.47f, 0.48f }; TestMixedGradientComponent(dataSize, inputLayer1, inputLayer2, expectedOutput, GradientSignal::MixedGradientLayer::MixingOperation::Initialize, 0.5f); } TEST_F(GradientSignalReferencesTestsFixture, MixedGradientComponent_OperationNormal) { // Mixed Gradient: Create two layers and set the second one to blend with "Normal" with an opacity of 0.5f. // Unlike "Initialize", this should blend the two layers based on the opacity. constexpr int dataSize = 3; AZStd::vector inputLayer1 = { 0.0f, 0.1f, 0.2f, 0.4f, 0.5f, 0.6f, 0.8f, 0.9f, 1.0f }; AZStd::vector inputLayer2 = { 0.06f, 0.16f, 0.26f, 0.46f, 0.56f, 0.66f, 0.86f, 0.94f, 0.96f }; // These values should be layer 2 * 0.5f, with no influence from layer 1. AZStd::vector expectedOutput = { 0.03f, 0.13f, 0.23f, 0.43f, 0.53f, 0.63f, 0.83f, 0.92f, 0.98f }; TestMixedGradientComponent(dataSize, inputLayer1, inputLayer2, expectedOutput, GradientSignal::MixedGradientLayer::MixingOperation::Normal, 0.5f); } TEST_F(GradientSignalReferencesTestsFixture, MixedGradientComponent_OperationMin) { // Mixed Gradient: Create two layers and set the second one to blend with "Min". // Tests a < b, a = b, a > b, and extreme ranges (0's and 1's) constexpr int dataSize = 3; AZStd::vector inputLayer1 = { 0.0f, 0.1f, 0.2f, 0.4f, 0.5f, 0.6f, 0.0f, 1.0f, 1.0f }; AZStd::vector inputLayer2 = { 0.2f, 0.2f, 0.2f, 0.4f, 0.4f, 0.4f, 1.0f, 0.0f, 1.0f }; AZStd::vector expectedOutput = { 0.0f, 0.1f, 0.2f, // layer 1 <= layer 2 0.4f, 0.4f, 0.4f, // layer 2 <= layer 1 0.0f, 0.0f, 1.0f // test the extremes }; TestMixedGradientComponent(dataSize, inputLayer1, inputLayer2, expectedOutput, GradientSignal::MixedGradientLayer::MixingOperation::Min, 1.0f); } TEST_F(GradientSignalReferencesTestsFixture, MixedGradientComponent_OperationMax) { // Mixed Gradient: Create two layers and set the second one to blend with "Max". // Tests a < b, a = b, a > b, and extreme ranges (0's and 1's) constexpr int dataSize = 3; AZStd::vector inputLayer1 = { 0.0f, 0.1f, 0.2f, 0.4f, 0.5f, 0.6f, 0.0f, 1.0f, 1.0f }; AZStd::vector inputLayer2 = { 0.2f, 0.2f, 0.2f, 0.4f, 0.4f, 0.4f, 1.0f, 0.0f, 1.0f }; AZStd::vector expectedOutput = { 0.2f, 0.2f, 0.2f, // layer 2 >= layer 1 0.4f, 0.5f, 0.6f, // layer 1 >= layer 2 1.0f, 1.0f, 1.0f // test the extremes }; TestMixedGradientComponent(dataSize, inputLayer1, inputLayer2, expectedOutput, GradientSignal::MixedGradientLayer::MixingOperation::Max, 1.0f); } TEST_F(GradientSignalReferencesTestsFixture, MixedGradientComponent_OperationAdd) { // Mixed Gradient: Create two layers and set the second one to blend with "Add". // Tests a + b = 0, a + b < 1, a + b = 1, and a + b > 1 (clamps to 1) constexpr int dataSize = 3; AZStd::vector inputLayer1 = { 0.0f, 0.1f, 0.2f, 0.4f, 0.5f, 0.6f, 0.8f, 0.9f, 1.0f }; AZStd::vector inputLayer2 = { 0.0f, 0.1f, 0.1f, 0.4f, 0.4f, 0.4f, 0.6f, 0.6f, 1.0f }; AZStd::vector expectedOutput = { 0.0f, 0.2f, 0.3f, 0.8f, 0.9f, 1.0f, 1.0f, 1.0f, 1.0f }; TestMixedGradientComponent(dataSize, inputLayer1, inputLayer2, expectedOutput, GradientSignal::MixedGradientLayer::MixingOperation::Add, 1.0f); } TEST_F(GradientSignalReferencesTestsFixture, MixedGradientComponent_OperationSubtract) { // Mixed Gradient: Create two layers and set the second one to blend with "Subtract". // Tests a - b = 0, a - b = 1, a - b > 0, and a - b < 0 (clamps to 0) constexpr int dataSize = 3; AZStd::vector inputLayer1 = { 0.0f, 0.3f, 1.0f, 0.5f, 0.7f, 1.0f, 0.5f, 0.4f, 0.3f }; AZStd::vector inputLayer2 = { 0.0f, 0.3f, 0.0f, 0.4f, 0.5f, 0.6f, 0.8f, 0.9f, 1.0f }; AZStd::vector expectedOutput = { 0.0f, 0.0f, 1.0f, // a - b = 0, a - b = 0, a - b = 1 0.1f, 0.2f, 0.4f, // a - b > 0 0.0f, 0.0f, 0.0f // a - b < 0 }; TestMixedGradientComponent(dataSize, inputLayer1, inputLayer2, expectedOutput, GradientSignal::MixedGradientLayer::MixingOperation::Subtract, 1.0f); } TEST_F(GradientSignalReferencesTestsFixture, MixedGradientComponent_OperationMultiply) { // Mixed Gradient: Create two layers and set the second one to blend with "Multiply". // Tests a * 0 = 0, 0 * b = 0, a * 1 = a, 1 * b = b, a * b < 1 constexpr int dataSize = 3; AZStd::vector inputLayer1 = { 0.0f, 0.1f, 0.0f, 0.4f, 1.0f, 1.0f, 0.8f, 0.9f, 1.0f }; AZStd::vector inputLayer2 = { 0.0f, 0.0f, 0.2f, 1.0f, 0.5f, 1.0f, 0.6f, 0.3f, 0.5f }; AZStd::vector expectedOutput = { 0.0f, 0.0f, 0.0f, // 0 * 0 = 0, a * 0 = 0, 0 * b = 0 0.4f, 0.5f, 1.0f, // a * 1 = a, 1 * b = b, 1 * 1 = 1 0.48f, 0.27f, 0.5f // a * b = c }; TestMixedGradientComponent(dataSize, inputLayer1, inputLayer2, expectedOutput, GradientSignal::MixedGradientLayer::MixingOperation::Multiply, 1.0f); } TEST_F(GradientSignalReferencesTestsFixture, MixedGradientComponent_OperationAverage) { // Mixed Gradient: Create two layers and set the second one to blend with "Average". // Tests a < b, a > b, a = b, 0, 1 constexpr int dataSize = 3; AZStd::vector inputLayer1 = { 0.0f, 0.1f, 0.2f, 0.4f, 0.5f, 0.6f, 0.8f, 0.9f, 1.0f }; AZStd::vector inputLayer2 = { 0.0f, 0.5f, 0.6f, 0.2f, 0.0f, 0.2f, 0.8f, 0.9f, 1.0f }; AZStd::vector expectedOutput = { 0.0f, 0.3f, 0.4f, // 0, a < b 0.3f, 0.25f, 0.4f, // a > b 0.8f, 0.9f, 1.0f // a = b }; TestMixedGradientComponent(dataSize, inputLayer1, inputLayer2, expectedOutput, GradientSignal::MixedGradientLayer::MixingOperation::Average, 1.0f); } TEST_F(GradientSignalReferencesTestsFixture, MixedGradientComponent_OperationOverlay) { // Mixed Gradient: Create two layers and set the second one to blend with "Overlay". // When a < 0.5, the output should be 2 * a * b // When a > 0.5, the output should be (1 - (2 * (1 - a) * (1 - b))) // (At a = 0.5, both formulas are equivalent) constexpr int dataSize = 3; AZStd::vector inputLayer1 = { 0.0f, 0.1f, 0.2f, 0.5f, 0.6f, 0.7f, 1.0f, 0.9f, 1.0f }; AZStd::vector inputLayer2 = { 0.1f, 0.4f, 0.8f, 0.9f, 0.2f, 0.3f, 0.7f, 1.0f, 1.0f }; AZStd::vector expectedOutput = { 0.0f, 0.08f, 0.32f, // a < 0.5, 2 * a * b 0.9f, 0.36f, 0.58f, // a >= 0.5, (1 - (2 * (1 - a) * (1 - b))) 1.0f, 1.0f, 1.0f // if a > 0.5 and a or b = 1, the result should be 1 }; TestMixedGradientComponent(dataSize, inputLayer1, inputLayer2, expectedOutput, GradientSignal::MixedGradientLayer::MixingOperation::Overlay, 1.0f); } TEST_F(GradientSignalReferencesTestsFixture, ReferenceGradientComponent_KnownValues) { // Verify that the Reference Gradient successfully "passes through" and provides back the // exact same values as the gradient it's referencing. constexpr int dataSize = 2; AZStd::vector inputData = { 0.0f, 1.0f, 0.2f, 0.1122f }; AZStd::vector expectedOutput = inputData; auto mockReference = CreateEntity(); const AZ::EntityId id = mockReference->GetId(); MockGradientArrayRequestsBus mockGradientRequestsBus(id, inputData, dataSize); GradientSignal::ReferenceGradientConfig config; config.m_gradientSampler.m_gradientId = mockReference->GetId(); auto entity = CreateEntity(); CreateComponent(entity.get(), config); ActivateEntity(entity.get()); TestFixedDataSampler(expectedOutput, dataSize, entity->GetId()); } TEST_F(GradientSignalReferencesTestsFixture, ReferenceGradientComponent_CyclicReferences) { // Verify that gradient references can validate and disconnect cyclic connections auto constantGradientEntity = CreateEntity(); GradientSignal::ConstantGradientConfig constantGradientConfig; CreateComponent(constantGradientEntity.get(), constantGradientConfig); ActivateEntity(constantGradientEntity.get()); // Verify cyclic reference test passes when pointing to gradient generator entity auto referenceGradientEntity1 = CreateEntity(); GradientSignal::ReferenceGradientConfig referenceGradientConfig1; referenceGradientConfig1.m_gradientSampler.m_ownerEntityId = referenceGradientEntity1->GetId(); referenceGradientConfig1.m_gradientSampler.m_gradientId = constantGradientEntity->GetId(); CreateComponent(referenceGradientEntity1.get(), referenceGradientConfig1); ActivateEntity(referenceGradientEntity1.get()); EXPECT_TRUE(referenceGradientConfig1.m_gradientSampler.ValidateGradientEntityId()); // Verify cyclic reference test passes when nesting references to gradient generator entity auto referenceGradientEntity2 = CreateEntity(); GradientSignal::ReferenceGradientConfig referenceGradientConfig2; referenceGradientConfig2.m_gradientSampler.m_ownerEntityId = referenceGradientEntity2->GetId(); referenceGradientConfig2.m_gradientSampler.m_gradientId = referenceGradientEntity1->GetId(); CreateComponent(referenceGradientEntity2.get(), referenceGradientConfig2); ActivateEntity(referenceGradientEntity2.get()); EXPECT_TRUE(referenceGradientConfig2.m_gradientSampler.ValidateGradientEntityId()); // Verify cyclic reference test fails when referencing self auto referenceGradientEntity3 = CreateEntity(); GradientSignal::ReferenceGradientConfig referenceGradientConfig3; referenceGradientConfig3.m_gradientSampler.m_ownerEntityId = referenceGradientEntity3->GetId(); referenceGradientConfig3.m_gradientSampler.m_gradientId = referenceGradientEntity3->GetId(); CreateComponent(referenceGradientEntity3.get(), referenceGradientConfig3); ActivateEntity(referenceGradientEntity3.get()); EXPECT_FALSE(referenceGradientConfig3.m_gradientSampler.ValidateGradientEntityId()); EXPECT_EQ(referenceGradientConfig3.m_gradientSampler.m_gradientId, AZ::EntityId()); // Verify cyclic reference test fails with nested, circular reference auto referenceGradientEntity4 = CreateEntity(); auto referenceGradientEntity5 = CreateEntity(); auto referenceGradientEntity6 = CreateEntity(); GradientSignal::ReferenceGradientConfig referenceGradientConfig4; referenceGradientConfig4.m_gradientSampler.m_ownerEntityId = referenceGradientEntity4->GetId(); referenceGradientConfig4.m_gradientSampler.m_gradientId = referenceGradientEntity5->GetId(); CreateComponent(referenceGradientEntity4.get(), referenceGradientConfig4); ActivateEntity(referenceGradientEntity4.get()); GradientSignal::ReferenceGradientConfig referenceGradientConfig5; referenceGradientConfig5.m_gradientSampler.m_ownerEntityId = referenceGradientEntity5->GetId(); referenceGradientConfig5.m_gradientSampler.m_gradientId = referenceGradientEntity6->GetId(); CreateComponent(referenceGradientEntity5.get(), referenceGradientConfig5); ActivateEntity(referenceGradientEntity5.get()); GradientSignal::ReferenceGradientConfig referenceGradientConfig6; referenceGradientConfig6.m_gradientSampler.m_ownerEntityId = referenceGradientEntity6->GetId(); referenceGradientConfig6.m_gradientSampler.m_gradientId = referenceGradientEntity4->GetId(); CreateComponent(referenceGradientEntity6.get(), referenceGradientConfig6); ActivateEntity(referenceGradientEntity6.get()); EXPECT_FALSE(referenceGradientConfig6.m_gradientSampler.ValidateGradientEntityId()); EXPECT_EQ(referenceGradientConfig6.m_gradientSampler.m_gradientId, AZ::EntityId()); } TEST_F(GradientSignalReferencesTestsFixture, ShapeAreaFalloffGradientComponent_ZeroFalloff) { // Verify that if we have a 0-width falloff, only the points that fall directly on the shape // get a 1, and everything else gets a 0 constexpr int dataSize = 3; AZStd::vector expectedOutput = { 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f }; // Create an AABB from -1 to 1, so points at coorindates 0 and 1 fall on it, but any points at coordinate 2 won't. auto entityShape = CreateEntity(); CreateComponent(entityShape.get()); MockShapeComponentHandler mockShapeComponentHandler(entityShape->GetId()); mockShapeComponentHandler.m_GetEncompassingAabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-1.0f), AZ::Vector3(1.0f)); GradientSignal::ShapeAreaFalloffGradientConfig config; config.m_shapeEntityId = entityShape->GetId(); config.m_falloffWidth = 0.0f; config.m_falloffType = GradientSignal::FalloffType::Outer; auto entity = CreateEntity(); CreateComponent(entity.get(), config); ActivateEntity(entity.get()); TestFixedDataSampler(expectedOutput, dataSize, entity->GetId()); } TEST_F(GradientSignalReferencesTestsFixture, ShapeAreaFalloffGradientComponent_NonZeroFalloff) { // Verify for a range of non-zero falloffs that we get back expected 1-0 values across the falloff range. // We should get 1 on the shape, and "falloff" down to 0 as we get further away. // For this test, we put the corner of our shape at (0, 0) so that everything past (0, 0) is falloff. // Create our test shape from -1 to 0, so we have a corner directly on (0, 0). auto entityShape = CreateEntity(); CreateComponent(entityShape.get()); MockShapeComponentHandler mockShapeComponentHandler(entityShape->GetId()); mockShapeComponentHandler.m_GetEncompassingAabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-1.0f), AZ::Vector3(0.0f)); // Run through a range of falloffs for (float falloff = 1.0f; falloff <= 5.0f; falloff++) { // Choose a dataSize larger than our largest tested falloff value to additionally test that // we get consistent 0 values eveywhere past the falloff distance. constexpr int dataSize = 7; AZStd::vector expectedOutput; GradientSignal::ShapeAreaFalloffGradientConfig config; config.m_shapeEntityId = entityShape->GetId(); config.m_falloffWidth = falloff; config.m_falloffType = GradientSignal::FalloffType::Outer; // To determine our expected output, we get the distance from (0, 0) and inverse lerp across the falloff - 0 range // to convert into our expected 0 - 1 output value range. for (int y = 0; y < dataSize; y++) { for (int x = 0; x < dataSize; x++) { // Get the number of meters away from the corner of the AABB sitting at (0, 0). float distance = AZ::Vector3::CreateZero().GetDistance(AZ::Vector3(static_cast(x), static_cast(y), 0.0f)); // We inverse lerp from falloff - 0 so that our values go from 1 at 0 distance down to 0 at falloff distance. expectedOutput.push_back(AZ::GetClamp(AZ::LerpInverse(config.m_falloffWidth, 0.0f, distance), 0.0f, 1.0f)); } } auto entity = CreateEntity(); CreateComponent(entity.get(), config); ActivateEntity(entity.get()); TestFixedDataSampler(expectedOutput, dataSize, entity->GetId()); } } TEST_F(GradientSignalReferencesTestsFixture, SurfaceAltitudeGradientComponent_PinnedShape) { // When using a Surface Altitude Gradient with a pinned shape, the altitude values that // come back should be based on the AABB range of the pinned shape. constexpr int dataSize = 2; AZStd::vector expectedOutput = { 0.0f, 0.2f, 0.5f, 1.0f, }; // We're pinning a shape, so the bounding box of (0, 0, 0) - (10, 10, 10) will be the one that applies. auto entityShape = CreateEntity(); CreateComponent(entityShape.get()); MockShapeComponentHandler mockShapeComponentHandler(entityShape->GetId()); mockShapeComponentHandler.m_GetEncompassingAabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3::CreateZero(), AZ::Vector3(10.0f)); // Set a different altitude for each point we're going to test. We'll use 0, 2, 5, 10 to test various points along the range. MockSurfaceDataSystem mockSurfaceDataSystem; mockSurfaceDataSystem.m_GetSurfacePoints[AZStd::make_pair(0.0f, 0.0f)] = { { entityShape->GetId(), AZ::Vector3(0.0f, 0.0f, 0.0f), AZ::Vector3::CreateZero() } }; mockSurfaceDataSystem.m_GetSurfacePoints[AZStd::make_pair(1.0f, 0.0f)] = { { entityShape->GetId(), AZ::Vector3(0.0f, 0.0f, 2.0f), AZ::Vector3::CreateZero() } }; mockSurfaceDataSystem.m_GetSurfacePoints[AZStd::make_pair(0.0f, 1.0f)] = { { entityShape->GetId(), AZ::Vector3(0.0f, 0.0f, 5.0f), AZ::Vector3::CreateZero() } }; mockSurfaceDataSystem.m_GetSurfacePoints[AZStd::make_pair(1.0f, 1.0f)] = { { entityShape->GetId(), AZ::Vector3(0.0f, 0.0f, 10.0f), AZ::Vector3::CreateZero() } }; // We set the min/max to values other than 0-10 to help validate that they aren't used in the case of the pinned shape. GradientSignal::SurfaceAltitudeGradientConfig config; config.m_shapeEntityId = entityShape->GetId(); config.m_altitudeMin = 1.0f; config.m_altitudeMax = 24.0f; auto entity = CreateEntity(); CreateComponent(entity.get(), config); ActivateEntity(entity.get()); TestFixedDataSampler(expectedOutput, dataSize, entity->GetId()); } TEST_F(GradientSignalReferencesTestsFixture, SurfaceAltitudeGradientComponent_NoShape) { // When using a Surface Altitude Gradient without a shape, the altitude values that // come back should be based on the min / max range of the component. constexpr int dataSize = 2; AZStd::vector expectedOutput = { 0.0f, 0.2f, 0.5f, 1.0f, }; auto entityShape = CreateEntity(); // Set a different altitude for each point we're going to test. We'll use 0, 2, 5, 10 to test various points along the range. MockSurfaceDataSystem mockSurfaceDataSystem; mockSurfaceDataSystem.m_GetSurfacePoints[AZStd::make_pair(0.0f, 0.0f)] = { { entityShape->GetId(), AZ::Vector3(0.0f, 0.0f, 0.0f), AZ::Vector3::CreateZero() } }; mockSurfaceDataSystem.m_GetSurfacePoints[AZStd::make_pair(1.0f, 0.0f)] = { { entityShape->GetId(), AZ::Vector3(0.0f, 0.0f, 2.0f), AZ::Vector3::CreateZero() } }; mockSurfaceDataSystem.m_GetSurfacePoints[AZStd::make_pair(0.0f, 1.0f)] = { { entityShape->GetId(), AZ::Vector3(0.0f, 0.0f, 5.0f), AZ::Vector3::CreateZero() } }; mockSurfaceDataSystem.m_GetSurfacePoints[AZStd::make_pair(1.0f, 1.0f)] = { { entityShape->GetId(), AZ::Vector3(0.0f, 0.0f, 10.0f), AZ::Vector3::CreateZero() } }; // We set the min/max to 0-10, but don't set a shape. GradientSignal::SurfaceAltitudeGradientConfig config; config.m_altitudeMin = 0.0f; config.m_altitudeMax = 10.0f; auto entity = CreateEntity(); CreateComponent(entity.get(), config); ActivateEntity(entity.get()); TestFixedDataSampler(expectedOutput, dataSize, entity->GetId()); } TEST_F(GradientSignalReferencesTestsFixture, SurfaceAltitudeGradientComponent_MissingSurfaceIsZero) { // Querying altitude where the surface doesn't exist results in a value of 0. constexpr int dataSize = 2; AZStd::vector expectedOutput = { 0.0f, 0.0f, 0.0f, 0.0f, }; auto entityShape = CreateEntity(); // Don't set any points. MockSurfaceDataSystem mockSurfaceDataSystem; // We set the min/max to -5 - 15 so that a height of 0 would produce a non-zero value. GradientSignal::SurfaceAltitudeGradientConfig config; config.m_altitudeMin = -5.0f; config.m_altitudeMax = 15.0f; auto entity = CreateEntity(); CreateComponent(entity.get(), config); ActivateEntity(entity.get()); TestFixedDataSampler(expectedOutput, dataSize, entity->GetId()); } TEST_F(GradientSignalReferencesTestsFixture, SurfaceAltitudeGradientComponent_ClampToMinMax) { // Verify that surface altitudes outside of the min / max range get clamped to 0.0 and 1.0. constexpr int dataSize = 2; AZStd::vector expectedOutput = { 0.0f, 0.0f, 1.0f, 1.0f, }; auto entityShape = CreateEntity(); MockSurfaceDataSystem mockSurfaceDataSystem; // Altitude value below min - should result in 0.0f. mockSurfaceDataSystem.m_GetSurfacePoints[AZStd::make_pair(0.0f, 0.0f)] = { { entityShape->GetId(), AZ::Vector3(0.0f, 0.0f, -10.0f), AZ::Vector3::CreateZero() } }; // Altitude value at exactly min - should result in 0.0f. mockSurfaceDataSystem.m_GetSurfacePoints[AZStd::make_pair(1.0f, 0.0f)] = { { entityShape->GetId(), AZ::Vector3(0.0f, 0.0f, -5.0f), AZ::Vector3::CreateZero() } }; // Altitude value at exactly max - should result in 1.0f. mockSurfaceDataSystem.m_GetSurfacePoints[AZStd::make_pair(0.0f, 1.0f)] = { { entityShape->GetId(), AZ::Vector3(0.0f, 0.0f, 15.0f), AZ::Vector3::CreateZero() } }; // Altitude value above max - should result in 1.0f. mockSurfaceDataSystem.m_GetSurfacePoints[AZStd::make_pair(1.0f, 1.0f)] = { { entityShape->GetId(), AZ::Vector3(0.0f, 0.0f, 20.0f), AZ::Vector3::CreateZero() } }; // We set the min/max to -5 - 15. By using a range without 0 at either end, and not having 0 as the midpoint, // it should be easier to verify that we're successfully clamping to 0 and 1. GradientSignal::SurfaceAltitudeGradientConfig config; config.m_altitudeMin = -5.0f; config.m_altitudeMax = 15.0f; auto entity = CreateEntity(); CreateComponent(entity.get(), config); ActivateEntity(entity.get()); TestFixedDataSampler(expectedOutput, dataSize, entity->GetId()); } TEST_F(GradientSignalReferencesTestsFixture, SurfaceMaskGradientComponent_SingleMaskExpectedValues) { // When querying a surface that contains the expected mask, verify we get back exactly the // values we expect for each point. constexpr int dataSize = 2; AZStd::vector expectedOutput = { 0.0f, 0.2f, 0.5f, 1.0f, }; MockSurfaceDataSystem mockSurfaceDataSystem; SurfaceData::SurfacePoint point; // Fill our mock surface with the test_mask set and the expected gradient value at each point. for (int y = 0; y < dataSize; y++) { for (int x = 0; x < dataSize; x++) { point.m_masks[AZ_CRC("test_mask", 0x7a16e9ff)] = expectedOutput[(y * dataSize) + x]; mockSurfaceDataSystem.m_GetSurfacePoints[AZStd::make_pair(static_cast(x), static_cast(y))] = { { point } }; } } GradientSignal::SurfaceMaskGradientConfig config; config.m_surfaceTagList.push_back(AZ_CRC("test_mask", 0x7a16e9ff)); auto entity = CreateEntity(); CreateComponent(entity.get(), config); ActivateEntity(entity.get()); TestFixedDataSampler(expectedOutput, dataSize, entity->GetId()); } TEST_F(GradientSignalReferencesTestsFixture, SurfaceMaskGradientComponent_NoValues) { // When querying a surface that contains no points (either lack of surface, or filtered-out surface tag), verify we get back 0.0f. // NOTE: Because we're mocking the SurfaceDataSystem, which is the system that contains the mask filtering logic, // we don't have separate tests for wrong mask vs no points. From the gradient's perspective, these should both // get no points returned from the system. constexpr int dataSize = 2; AZStd::vector expectedOutput = { 0.0f, 0.0f, 0.0f, 0.0f, }; MockSurfaceDataSystem mockSurfaceDataSystem; GradientSignal::SurfaceMaskGradientConfig config; config.m_surfaceTagList.push_back(AZ_CRC("test_mask", 0x7a16e9ff)); auto entity = CreateEntity(); CreateComponent(entity.get(), config); ActivateEntity(entity.get()); TestFixedDataSampler(expectedOutput, dataSize, entity->GetId()); } TEST_F(GradientSignalReferencesTestsFixture, SurfaceSlopeGradientComponent_KnownValues) { // When using a Surface Slope Gradient, verify that we get back expected slope values // for given sets of normals and min / max ranges. constexpr int dataSize = 3; AZStd::vector expectedOutput = { 1.0f, 0.9f, 0.8f, 0.6f, 0.5f, 0.4f, 0.2f, 0.1f, 0.0f }; AZStd::vector> minMaxTests = { AZStd::make_pair(0.0f, 90.0f), // Test the regular full min/max range (note that values above 90 degrees aren't supported) AZStd::make_pair(90.0f, 0.0f), // Test an inverted min/max range AZStd::make_pair(10.0f, 70.0f) // Test an asymmetric range within the full 0 - 90 degree range. }; AZStd::vector rampTests = { GradientSignal::SurfaceSlopeGradientConfig::RampType::LINEAR_RAMP_DOWN, GradientSignal::SurfaceSlopeGradientConfig::RampType::LINEAR_RAMP_UP, }; for (auto rampTest : rampTests) { for (auto minMax : minMaxTests) { AZStd::vector inputAngles; const float slopeMin = minMax.first; const float slopeMax = minMax.second; // Fill our mock surface with normals that match the correct test angle for each point. for (int y = 0; y < dataSize; y++) { for (int x = 0; x < dataSize; x++) { float angle = 0.0f; // For linear ramps, the input angle should be whatever our desired output is, // lerped either between slopeMin-slopeMax, or slopeMax-slopeMin, depending on the // direction of the ramp. switch (rampTest) { case GradientSignal::SurfaceSlopeGradientConfig::RampType::LINEAR_RAMP_DOWN: angle = AZ::Lerp(slopeMax, slopeMin, expectedOutput[(y * dataSize) + x]); break; case GradientSignal::SurfaceSlopeGradientConfig::RampType::LINEAR_RAMP_UP: angle = AZ::Lerp(slopeMin, slopeMax, expectedOutput[(y * dataSize) + x]); break; } inputAngles.push_back(angle); } } TestSurfaceSlopeGradientComponent(dataSize, inputAngles, expectedOutput, slopeMin, slopeMax, rampTest, 0.0f, 0.0f, 0.0f); } } } TEST_F(GradientSignalReferencesTestsFixture, SurfaceSlopeGradientComponent_ClampToMinMax) { // Verify that surface slope outside of the min / max range get clamped to 1.0 and 0.0. // NOTE: We expect the Surface Slope Gradient to produce a signal value of 1.0 at or below the min, // and 0.0 at or above the max. constexpr int dataSize = 2; AZStd::vector inputAngles = { 5.0f, 20.0f, // test that values below or at the min clamp to 1.0 50.0f, 70.0f, // test that values at or above the max clamp to 0.0 }; AZStd::vector expectedOutput = { 1.0f, 1.0f, 0.0f, 0.0f, }; // We set the min/max to 20 - 50 as a mostly arbitrary choice that represents a range that's not // centered around the midpoint of a full 0 - 90 degree range. TestSurfaceSlopeGradientComponent(dataSize, inputAngles, expectedOutput, 20.0f, 50.0f, GradientSignal::SurfaceSlopeGradientConfig::RampType::LINEAR_RAMP_DOWN, 0.0f, 0.0f, 0.0f); } TEST_F(GradientSignalReferencesTestsFixture, SurfaceSlopeGradientComponent_SmoothStep) { // Verify that surface slope produces expected results when used with a smooth step. // Smooth step creates a ramp up and down. We expect the following (within our min/max angle range): // inputs 0 to (midpoint - range/2): 0 // inputs (midpoint - range/2) to (midpoint - range/2)+softness: ramp up // inputs (midpoint - range/2)+softness to (midpoint + range/2)-softness: 1 // inputs (midpoint + range/2)-softness) to (midpoint + range/2): ramp down // inputs (midpoint + range/2) to 1: 0 // We'll test with midpoint = 0.5, range = 0.6, softness = 0.1 so that we have easy ranges to verify. constexpr int dataSize = 5; AZStd::vector inputData = { 0.00f, 0.05f, 0.10f, 0.15f, 0.20f, // Should all be 0 0.21f, 0.23f, 0.25f, 0.27f, 0.29f, // Should ramp up 0.30f, 0.40f, 0.50f, 0.60f, 0.70f, // Should all be 1 0.71f, 0.73f, 0.75f, 0.77f, 0.79f, // Should ramp down 0.80f, 0.85f, 0.90f, 0.95f, 1.00f // Should all be 0 }; // For smoothstep ramp curves, we expect the values to be symmetric between the up and down ramp, // hit 0.5 at the middle of the ramp, and be symmetric on both sides of the midpoint of the ramp. AZStd::vector expectedOutput = { 0.000f, 0.000f, 0.000f, 0.000f, 0.000f, // 0.00 - 0.20 input -> 0.0 output 0.028f, 0.216f, 0.500f, 0.784f, 0.972f, // 0.21 - 0.29 input -> pre-verified ramp up values 1.000f, 1.000f, 1.000f, 1.000f, 1.000f, // 0.30 - 0.70 input -> 1.0 output 0.972f, 0.784f, 0.500f, 0.216f, 0.028f, // 0.71 - 0.79 input -> pre-verified ramp down values 0.000f, 0.000f, 0.000f, 0.000f, 0.000f, // 0.80 - 1.00 input -> 0.0 output }; AZStd::vector inputAngles; // We set the min/max to 20 - 50 as a mostly arbitrary choice that represents a range that's not // centered around the midpoint of a full 0 - 90 degree range. const float slopeMin = 20.0f; const float slopeMax = 50.0f; // Fill our mock surface with the correct normal value for each point based on our test angle set. for (int y = 0; y < dataSize; y++) { for (int x = 0; x < dataSize; x++) { // Map our input values of 0-1 into our slope Min-Max range to create our desired input angles. inputAngles.push_back(AZ::Lerp(slopeMin, slopeMax, inputData[(y * dataSize) + x])); } } TestSurfaceSlopeGradientComponent(dataSize, inputAngles, expectedOutput, slopeMin, slopeMax, GradientSignal::SurfaceSlopeGradientConfig::RampType::SMOOTH_STEP, 0.5f, 0.6f, 0.1f); } TEST_F(GradientSignalReferencesTestsFixture, SurfaceSlopeGradientComponent_SmoothStep_ClampToZero) { // Verify that surface slope outside of the min / max range get clamped to 0.0 when using smooth step. constexpr int dataSize = 2; AZStd::vector inputAngles = { 5.0f, 20.0f, // test that values below or at the min clamp to 0.0 50.0f, 70.0f, // test that values at or above the max clamp to 0.0 }; AZStd::vector expectedOutput = { 0.0f, 0.0f, 0.0f, 0.0f, }; TestSurfaceSlopeGradientComponent(dataSize, inputAngles, expectedOutput, 20.0f, 50.0f, GradientSignal::SurfaceSlopeGradientConfig::RampType::SMOOTH_STEP, 0.5f, 0.6f, 0.1f); } }