PaintBrushSmoothLocationTests.cpp 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include <AzCore/Serialization/SerializeContext.h>
  9. #include <AzCore/UnitTest/TestTypes.h>
  10. #include <AzTest/AzTest.h>
  11. #include <AzFramework/PaintBrush/PaintBrush.h>
  12. #include <AZTestShared/Math/MathTestHelpers.h>
  13. #include <Tests/PaintBrush/MockPaintBrushNotificationHandler.h>
  14. // This set of unit tests validates that the SmoothToLocation API trivially works and calculates location changes correctly.
  15. namespace UnitTest
  16. {
  17. class PaintBrushSmoothLocationTestFixture : public LeakDetectionFixture
  18. {
  19. public:
  20. // Some common default values that we'll use in our tests.
  21. const AZ::EntityComponentIdPair EntityComponentIdPair{ AZ::EntityId(123), AZ::ComponentId(456) };
  22. const AZ::Vector3 TestBrushCenter{ 10.0f, 20.0f, 30.0f };
  23. const AZ::Vector3 TestBrushCenter2d{ 10.0f, 20.0f, 0.0f }; // This should be the same as TestBrushCenter, but with Z=0
  24. const AZ::Color TestColor{ 0.25f, 0.50f, 0.75f, 1.0f };
  25. // Useful helpers for our tests
  26. AzFramework::PaintBrushSettings m_settings;
  27. };
  28. TEST_F(PaintBrushSmoothLocationTestFixture, SmoothToLocationAtSingleLocationFunctionsCorrectly)
  29. {
  30. // This tests all of the basic SmoothToLocation() functionality:
  31. // - It will call OnSmooth with the correct dirty area for the brush settings and initial location
  32. // - The valueLookupFn will only return valid points that occur within the brush.
  33. // - The smoothFn will smooth values together.
  34. AzFramework::PaintBrush paintBrush(EntityComponentIdPair);
  35. ::testing::NiceMock<MockPaintBrushNotificationBusHandler> mockHandler(EntityComponentIdPair);
  36. const float TestBrushRadius = 1.0f;
  37. m_settings.SetSize(TestBrushRadius * 2.0f);
  38. // We'll set the smooth mode to "Mean" just so that it's easy to verify that the smoothFn trivially works.
  39. m_settings.SetSmoothMode(AzFramework::PaintBrushSmoothMode::Mean);
  40. EXPECT_CALL(mockHandler, OnSmooth(::testing::_, ::testing::_, ::testing::_, ::testing::_, ::testing::_)).Times(1);
  41. ON_CALL(mockHandler, OnSmooth)
  42. .WillByDefault(
  43. [this, TestBrushRadius]([[maybe_unused]] const AZ::Color& color,
  44. const AZ::Aabb& dirtyArea,
  45. AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn,
  46. AZStd::span<const AZ::Vector3> valuePointOffsets,
  47. AzFramework::PaintBrushNotifications::SmoothFn& smoothFn)
  48. {
  49. // The OnSmooth method for a listener to the PaintBrushNotificationBus should work as follows:
  50. // - It should receive a dirtyArea AABB that contains the region that's been smoothed.
  51. // - For each point that the listener cares about in that region, it should call valueLookupFn() to find
  52. // out which points actually fall within the paintbrush, and what the opacities of those points are.
  53. // - For each valid point and opacity, the listener should gather all of the points around the point based
  54. // on the relative valuePointOffsets, and then call smoothFn with all of those points to get a smoothed value.
  55. // Validate the dirtyArea AABB:
  56. // We expect the AABB to be centered around the TestBrushCenter but with a Z value of 0
  57. // because we only support painting in 2D for now. The radius of the AABB should match the radius of our paintbrush.
  58. EXPECT_THAT(dirtyArea, IsClose(AZ::Aabb::CreateCenterRadius(TestBrushCenter2d, TestBrushRadius)));
  59. // Validate the valueLookupFn:
  60. // Create a 3x3 square grid of points. Because our brush is a circle, we expect only the points in along a + to be
  61. // returned as valid points. The corners of the square should fall outside the circle and not get returned.
  62. const AZStd::vector<AZ::Vector3> points = {
  63. AZ::Vector3(dirtyArea.GetMin().GetX(), dirtyArea.GetMin().GetY(), 0.0f),
  64. AZ::Vector3(dirtyArea.GetCenter().GetX(), dirtyArea.GetMin().GetY(), 0.0f),
  65. AZ::Vector3(dirtyArea.GetMax().GetX(), dirtyArea.GetMin().GetY(), 0.0f),
  66. AZ::Vector3(dirtyArea.GetMin().GetX(), dirtyArea.GetCenter().GetY(), 0.0f),
  67. AZ::Vector3(dirtyArea.GetCenter().GetX(), dirtyArea.GetCenter().GetY(), 0.0f),
  68. AZ::Vector3(dirtyArea.GetMax().GetX(), dirtyArea.GetCenter().GetY(), 0.0f),
  69. AZ::Vector3(dirtyArea.GetMin().GetX(), dirtyArea.GetMax().GetY(), 0.0f),
  70. AZ::Vector3(dirtyArea.GetCenter().GetX(), dirtyArea.GetMax().GetY(), 0.0f),
  71. AZ::Vector3(dirtyArea.GetMax().GetX(), dirtyArea.GetMax().GetY(), 0.0f),
  72. };
  73. AZStd::vector<AZ::Vector3> validPoints;
  74. AZStd::vector<float> opacities;
  75. valueLookupFn(points, validPoints, opacities);
  76. // We should only have the 5 points along the + in validPoints.
  77. const AZStd::vector<AZ::Vector3> expectedValidPoints = {
  78. AZ::Vector3(dirtyArea.GetCenter().GetX(), dirtyArea.GetMin().GetY(), 0.0f),
  79. AZ::Vector3(dirtyArea.GetMin().GetX(), dirtyArea.GetCenter().GetY(), 0.0f),
  80. AZ::Vector3(dirtyArea.GetCenter().GetX(), dirtyArea.GetCenter().GetY(), 0.0f),
  81. AZ::Vector3(dirtyArea.GetMax().GetX(), dirtyArea.GetCenter().GetY(), 0.0f),
  82. AZ::Vector3(dirtyArea.GetCenter().GetX(), dirtyArea.GetMax().GetY(), 0.0f),
  83. };
  84. EXPECT_THAT(validPoints, ::testing::Pointwise(ContainerIsClose(), expectedValidPoints));
  85. // We should only have 5 opacities, and they should all be 1.0 because we haven't adjusted any brush settings.
  86. EXPECT_EQ(opacities.size(), 5);
  87. for (auto& opacity : opacities)
  88. {
  89. EXPECT_NEAR(opacity, 1.0f, 0.001f);
  90. }
  91. // By default, the smoothing brush uses a 3x3 kernel, so we expect our relative offsets to be -1 to 1
  92. // in each direction.
  93. const AZStd::vector<AZ::Vector3> expectedPointOffsets = {
  94. AZ::Vector3(-1.0f, -1.0f, 0.0f), AZ::Vector3(0.0f, -1.0f, 0.0f), AZ::Vector3(1.0f, -1.0f, 0.0f),
  95. AZ::Vector3(-1.0f, 0.0f, 0.0f), AZ::Vector3(0.0f, 0.0f, 0.0f), AZ::Vector3(1.0f, 0.0f, 0.0f),
  96. AZ::Vector3(-1.0f, 1.0f, 0.0f), AZ::Vector3(0.0f, 1.0f, 0.0f), AZ::Vector3(1.0f, 1.0f, 0.0f),
  97. };
  98. EXPECT_THAT(valuePointOffsets, ::testing::Pointwise(ContainerIsClose(), expectedPointOffsets));
  99. const float baseValue = 1.0f;
  100. // We'll set our kernel to only have a single value of 1. The mean should be 1/9.
  101. float kernelValues[] = { 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f };
  102. const float expectedMean = 1.0f / 9.0f;
  103. // With full opacity, we should just get back the mean of our kernel values.
  104. const float smoothedValue = smoothFn(baseValue, kernelValues, 1.0f);
  105. EXPECT_NEAR(smoothedValue, expectedMean, 0.01f);
  106. // With half opacity, we should get back a value halfway between the mean and 1.0f.
  107. const float partialSmoothedValue = smoothFn(baseValue, kernelValues, 0.5f);
  108. EXPECT_NEAR(partialSmoothedValue, expectedMean + ((1.0f - expectedMean) / 2.0f), 0.01f);
  109. });
  110. paintBrush.BeginPaintMode();
  111. paintBrush.BeginBrushStroke(m_settings);
  112. paintBrush.SmoothToLocation(TestBrushCenter, m_settings);
  113. paintBrush.EndBrushStroke();
  114. paintBrush.EndPaintMode();
  115. }
  116. TEST_F(PaintBrushSmoothLocationTestFixture, SmoothToLocationWithSmallMovementDoesNotTriggerPainting)
  117. {
  118. // This verifies that if the distance between two SmoothToLocation calls is small enough, it won't trigger
  119. // an OnSmooth. "small" is defined as less than (brush size * distance %).
  120. AzFramework::PaintBrush paintBrush(EntityComponentIdPair);
  121. ::testing::NiceMock<MockPaintBrushNotificationBusHandler> mockHandler(EntityComponentIdPair);
  122. // Set the distance between brush stamps to 50%
  123. const float TestDistancePercent = 50.0f;
  124. m_settings.SetDistancePercent(TestDistancePercent);
  125. // Set the brush radius to 1 meter (diameter is 2 meters)
  126. const float TestBrushRadius = 1.0f;
  127. m_settings.SetSize(TestBrushRadius * 2.0f);
  128. // The distance we expect to need to move to trigger another paint call is 50% of our brush size.
  129. const float DistanceToTriggerSecondCall = TestBrushRadius * 2.0f * (TestDistancePercent / 100.0f);
  130. // Choose a second brush center location that's just slightly under the threshold that should be needed to trigger a second
  131. // OnPaint call.
  132. const AZ::Vector3 tooSmallSecondLocation = TestBrushCenter + AZ::Vector3(DistanceToTriggerSecondCall - 0.01f, 0.0f, 0.0f);
  133. // We expect to get called only once for our initial SmoothToLocation(); the second SmoothToLocation() won't
  134. // have moved far enough to trigger a second OnSmooth call.
  135. EXPECT_CALL(mockHandler, OnSmooth(::testing::_, ::testing::_, ::testing::_, ::testing::_, ::testing::_)).Times(1);
  136. paintBrush.BeginPaintMode();
  137. paintBrush.BeginBrushStroke(m_settings);
  138. paintBrush.SmoothToLocation(TestBrushCenter, m_settings);
  139. paintBrush.SmoothToLocation(tooSmallSecondLocation, m_settings);
  140. paintBrush.EndBrushStroke();
  141. paintBrush.EndPaintMode();
  142. // Try the test again, this time moving exactly the amount we need to so that we trigger a second call.
  143. // (We do this to verify that we've correctly identified the threshold under which we should not trigger another OnSmooth)
  144. const AZ::Vector3 largeEnoughSecondLocation = TestBrushCenter + AZ::Vector3(DistanceToTriggerSecondCall, 0.0f, 0.0f);
  145. EXPECT_CALL(mockHandler, OnSmooth(::testing::_, ::testing::_, ::testing::_, ::testing::_, ::testing::_)).Times(2);
  146. paintBrush.BeginPaintMode();
  147. paintBrush.BeginBrushStroke(m_settings);
  148. paintBrush.SmoothToLocation(TestBrushCenter, m_settings);
  149. paintBrush.SmoothToLocation(largeEnoughSecondLocation, m_settings);
  150. paintBrush.EndBrushStroke();
  151. paintBrush.EndPaintMode();
  152. }
  153. TEST_F(PaintBrushSmoothLocationTestFixture, SmoothToLocationSecondMovementDoesNotIncludeFirstCircle)
  154. {
  155. // When smoothing, the first SmoothToLocation call should just contain a single brush stamp at the passed-in location.
  156. // The second SmoothToLocation call should contain brush stamps from the first location to the second, but should NOT
  157. // have a second brush stamp at the first location. Ex:
  158. // O <- first SmoothToLocation
  159. // -OOO <- second SmoothToLocation
  160. // If the Distance % is anything less than 100% in the paint brush settings, the Os will overlap.
  161. // We'll set it to 100% just to make it obvious that we've gotten the correct result.
  162. AzFramework::PaintBrush paintBrush(EntityComponentIdPair);
  163. ::testing::NiceMock<MockPaintBrushNotificationBusHandler> mockHandler(EntityComponentIdPair);
  164. // Set the distance between brush stamps to 100%
  165. const float TestDistancePercent = 100.0f;
  166. m_settings.SetDistancePercent(TestDistancePercent);
  167. // Set the brush radius to 1 meter (diameter is 2 meters)
  168. const float TestBrushRadius = 1.0f;
  169. const float TestBrushSize = TestBrushRadius * 2.0f;
  170. m_settings.SetSize(TestBrushSize);
  171. // Choose a second brush center location that's 3 full brush stamps away. This should give us a total
  172. // of 4 brush stamps that get painted between the two calls.
  173. const AZ::Vector3 secondLocation = TestBrushCenter + AZ::Vector3(TestBrushSize * 3.0f, 0.0f, 0.0f);
  174. // We expect to get two OnSmooth calls.
  175. EXPECT_CALL(mockHandler, OnSmooth(::testing::_, ::testing::_, ::testing::_, ::testing::_, ::testing::_)).Times(2);
  176. paintBrush.BeginPaintMode();
  177. paintBrush.BeginBrushStroke(m_settings);
  178. ON_CALL(mockHandler, OnSmooth)
  179. .WillByDefault(
  180. [this, TestBrushRadius]([[maybe_unused]] const AZ::Color& color,
  181. const AZ::Aabb& dirtyArea,
  182. [[maybe_unused]] AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn,
  183. [[maybe_unused]] AZStd::span<const AZ::Vector3> valuePointOffsets,
  184. [[maybe_unused]] AzFramework::PaintBrushNotifications::SmoothFn& smoothFn)
  185. {
  186. // On the first SmoothToLocation, we expect to get a dirtyArea that exactly fits our first paint brush stamp.
  187. const AZ::Aabb expectedFirstDirtyArea = AZ::Aabb::CreateCenterRadius(TestBrushCenter2d, TestBrushRadius);
  188. EXPECT_THAT(dirtyArea, IsClose(expectedFirstDirtyArea));
  189. });
  190. paintBrush.SmoothToLocation(TestBrushCenter, m_settings);
  191. ON_CALL(mockHandler, OnSmooth)
  192. .WillByDefault(
  193. [this, TestBrushSize, TestBrushRadius]([[maybe_unused]] const AZ::Color& color,
  194. const AZ::Aabb& dirtyArea,
  195. [[maybe_unused]] AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn,
  196. [[maybe_unused]] AZStd::span<const AZ::Vector3> valuePointOffsets,
  197. [[maybe_unused]] AzFramework::PaintBrushNotifications::SmoothFn& smoothFn)
  198. {
  199. // On the second SmoothToLocation, we expect the dirtyArea to only contain the next 3 paint brush stamps,
  200. // but not the first one.
  201. const AZ::Vector3 stampIncrement = AZ::Vector3(TestBrushSize, 0.0f, 0.0f);
  202. AZ::Aabb expectedSecondDirtyArea = AZ::Aabb::CreateCenterRadius(TestBrushCenter2d + stampIncrement, TestBrushRadius);
  203. expectedSecondDirtyArea.AddAabb(
  204. AZ::Aabb::CreateCenterRadius(TestBrushCenter2d + (3.0f * stampIncrement), TestBrushRadius));
  205. EXPECT_THAT(dirtyArea, IsClose(expectedSecondDirtyArea));
  206. });
  207. paintBrush.SmoothToLocation(secondLocation, m_settings);
  208. paintBrush.EndBrushStroke();
  209. paintBrush.EndPaintMode();
  210. }
  211. TEST_F(PaintBrushSmoothLocationTestFixture, EyedropperDoesNotAffectSmoothToLocation)
  212. {
  213. // When smoothing, we should be able to call UseEyedropper at any arbitrary location without affecting the current
  214. // state of SmoothToLocation.
  215. AzFramework::PaintBrush paintBrush(EntityComponentIdPair);
  216. ::testing::NiceMock<MockPaintBrushNotificationBusHandler> mockHandler(EntityComponentIdPair);
  217. // Set the brush radius to 1 meter (diameter is 2 meters)
  218. const float TestBrushRadius = 1.0f;
  219. const float TestBrushSize = TestBrushRadius * 2.0f;
  220. m_settings.SetSize(TestBrushSize);
  221. // Choose a second brush center location that's 2 full brush stamps away in the X direction only.
  222. AZ::Vector3 secondLocation = TestBrushCenter + AZ::Vector3(TestBrushSize * 2.0f, 0.0f, 0.0f);
  223. // We expect to get two OnSmooth calls.
  224. EXPECT_CALL(mockHandler, OnSmooth(::testing::_, ::testing::_, ::testing::_, ::testing::_, ::testing::_)).Times(2);
  225. ON_CALL(mockHandler, OnSmooth)
  226. .WillByDefault(
  227. [this, TestBrushRadius]([[maybe_unused]] const AZ::Color& color,
  228. const AZ::Aabb& dirtyArea,
  229. [[maybe_unused]] AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn,
  230. [[maybe_unused]] AZStd::span<const AZ::Vector3> valuePointOffsets,
  231. [[maybe_unused]] AzFramework::PaintBrushNotifications::SmoothFn& smoothFn)
  232. {
  233. // We expect that the Y value for our dirtyArea won't be changed even though we'll call UseEyedropper
  234. // with a large Y value in-between the two paint calls.
  235. EXPECT_NEAR(dirtyArea.GetMin().GetY(), TestBrushCenter.GetY() - TestBrushRadius, 0.01f);
  236. EXPECT_NEAR(dirtyArea.GetMax().GetY(), TestBrushCenter.GetY() + TestBrushRadius, 0.01f);
  237. });
  238. paintBrush.BeginPaintMode();
  239. paintBrush.BeginBrushStroke(m_settings);
  240. paintBrush.SmoothToLocation(TestBrushCenter, m_settings);
  241. // Call UseEyeDropper with a large Y value so that we can easily detect if it affected our SmoothToLocation calls.
  242. [[maybe_unused]] AZ::Color color = paintBrush.UseEyedropper(TestBrushCenter + AZ::Vector3(0.0f, 1000.0f, 0.0f));
  243. paintBrush.SmoothToLocation(secondLocation, m_settings);
  244. paintBrush.EndBrushStroke();
  245. paintBrush.EndPaintMode();
  246. }
  247. TEST_F(PaintBrushSmoothLocationTestFixture, ResetBrushStrokeTrackingWorksCorrectly)
  248. {
  249. // If ResetBrushStrokeTracking is called in-between two calls to SmoothtToLocation within a brush stroke,
  250. // there should be a discontinuity between the two locations as if the brush has been picked up and put back down.
  251. // i.e. Instead of 'OOOOOO' between two locations it should create 'O O'.
  252. // This is typically used for handling things like leaving the edge of the image at one location and coming back onto
  253. // the image at a different location.
  254. AzFramework::PaintBrush paintBrush(EntityComponentIdPair);
  255. ::testing::NiceMock<MockPaintBrushNotificationBusHandler> mockHandler(EntityComponentIdPair);
  256. // Set the distance between brush stamps to 100%
  257. const float TestDistancePercent = 100.0f;
  258. m_settings.SetDistancePercent(TestDistancePercent);
  259. // Set the brush radius to 1 meter (diameter is 2 meters)
  260. const float TestBrushRadius = 1.0f;
  261. const float TestBrushSize = TestBrushRadius * 2.0f;
  262. m_settings.SetSize(TestBrushSize);
  263. // Choose a second brush center location that's 10 full brush stamps away to make it obvious whether or not
  264. // there are any points tracked between the two locations.
  265. const AZ::Vector3 secondLocation = TestBrushCenter + AZ::Vector3(TestBrushSize * 10.0f, 0.0f, 0.0f);
  266. // We expect to get two OnSmooth calls.
  267. EXPECT_CALL(mockHandler, OnSmooth(::testing::_, ::testing::_, ::testing::_, ::testing::_, ::testing::_)).Times(2);
  268. paintBrush.BeginPaintMode();
  269. paintBrush.BeginBrushStroke(m_settings);
  270. ON_CALL(mockHandler, OnSmooth)
  271. .WillByDefault(
  272. [this, TestBrushRadius]([[maybe_unused]] const AZ::Color& color,
  273. const AZ::Aabb& dirtyArea,
  274. [[maybe_unused]] AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn,
  275. [[maybe_unused]] AZStd::span<const AZ::Vector3> valuePointOffsets,
  276. [[maybe_unused]] AzFramework::PaintBrushNotifications::SmoothFn& smoothFn)
  277. {
  278. // On the first PaintToLocation, we expect to get a dirtyArea that exactly fits our first paint brush stamp.
  279. const AZ::Aabb expectedFirstDirtyArea = AZ::Aabb::CreateCenterRadius(TestBrushCenter2d, TestBrushRadius);
  280. EXPECT_THAT(dirtyArea, IsClose(expectedFirstDirtyArea));
  281. });
  282. paintBrush.SmoothToLocation(TestBrushCenter, m_settings);
  283. // Reset the brush stroke tracking, so that the next location will look like the start of a stroke again.
  284. paintBrush.ResetBrushStrokeTracking();
  285. ON_CALL(mockHandler, OnSmooth)
  286. .WillByDefault(
  287. [=]([[maybe_unused]] const AZ::Color& color,
  288. const AZ::Aabb& dirtyArea,
  289. [[maybe_unused]] AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn,
  290. [[maybe_unused]] AZStd::span<const AZ::Vector3> valuePointOffsets,
  291. [[maybe_unused]] AzFramework::PaintBrushNotifications::SmoothFn& smoothFn)
  292. {
  293. // On the second PaintToLocation, we expect to get a dirtyArea that exactly fits our second paint brush stamp.
  294. // It should *not* include any of the space bewteen the first and the second brush stamp.
  295. const AZ::Vector3 secondLocation2d(AZ::Vector2(secondLocation), 0.0f);
  296. const AZ::Aabb expectedSecondDirtyArea = AZ::Aabb::CreateCenterRadius(secondLocation2d, TestBrushRadius);
  297. EXPECT_THAT(dirtyArea, IsClose(expectedSecondDirtyArea));
  298. });
  299. paintBrush.SmoothToLocation(secondLocation, m_settings);
  300. paintBrush.EndBrushStroke();
  301. paintBrush.EndPaintMode();
  302. }
  303. } // namespace UnitTest