PaintBrushPaintSettingsTests.cpp 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846
  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 PaintToLocation/SmoothToLocation APIs use the paint brush settings correctly.
  15. namespace UnitTest
  16. {
  17. class PaintBrushPaintSettingsTestFixture : 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. // Verify that for whatever PaintBrushSettings have already been set, that both PaintToLocation() and SmoothToLocation()
  28. // won't produce any notifications when they're triggered, because the settings won't produce any valid points.
  29. void TestZeroNotificationsForPaintAndSmooth()
  30. {
  31. AzFramework::PaintBrush paintBrush(EntityComponentIdPair);
  32. ::testing::NiceMock<MockPaintBrushNotificationBusHandler> mockHandler(EntityComponentIdPair);
  33. EXPECT_CALL(mockHandler, OnPaint(::testing::_, ::testing::_, ::testing::_, ::testing::_)).Times(0);
  34. EXPECT_CALL(mockHandler, OnSmooth(::testing::_, ::testing::_, ::testing::_, ::testing::_, ::testing::_)).Times(0);
  35. paintBrush.BeginPaintMode();
  36. paintBrush.BeginBrushStroke(m_settings);
  37. paintBrush.PaintToLocation(TestBrushCenter, m_settings);
  38. paintBrush.EndBrushStroke();
  39. paintBrush.BeginBrushStroke(m_settings);
  40. paintBrush.SmoothToLocation(TestBrushCenter, m_settings);
  41. paintBrush.EndBrushStroke();
  42. paintBrush.EndPaintMode();
  43. }
  44. // Validate that both PaintToLocation() and SmoothToLocation() behave the same way for the previously set PaintBrushSettings.
  45. // This validation only checks for valid dirtyArea and valueLookupFn results, which are common to both OnPaint() and OnSmooth()
  46. // notifications. The *ToLocation() call will be called once for each validationFn provided.
  47. using ValidationFn =
  48. AZStd::function<void(const AZ::Aabb& dirtyArea, AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn)>;
  49. void ValidatePaintAndSmooth(
  50. AzFramework::PaintBrush& paintBrush,
  51. ::testing::NiceMock<MockPaintBrushNotificationBusHandler>& mockHandler,
  52. AZStd::span<const AZ::Vector3> locations,
  53. AZStd::span<ValidationFn> validationFns)
  54. {
  55. AZ_Assert(locations.size() == validationFns.size(), "We should have one location for each validationFn passed in.");
  56. paintBrush.BeginPaintMode();
  57. // Verify that PaintToLocation() validates correctly.
  58. paintBrush.BeginBrushStroke(m_settings);
  59. for (size_t index = 0; index < locations.size(); index++)
  60. {
  61. EXPECT_CALL(mockHandler, OnPaint(::testing::_, ::testing::_, ::testing::_, ::testing::_)).Times(1);
  62. ON_CALL(mockHandler, OnPaint)
  63. .WillByDefault(
  64. [=]([[maybe_unused]] const AZ::Color& color,
  65. const AZ::Aabb& dirtyArea,
  66. AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn,
  67. [[maybe_unused]] AzFramework::PaintBrushNotifications::BlendFn& blendFn)
  68. {
  69. validationFns[index](dirtyArea, valueLookupFn);
  70. });
  71. paintBrush.PaintToLocation(locations[index], m_settings);
  72. }
  73. paintBrush.EndBrushStroke();
  74. // Verify that SmoothToLocation() validates correctly.
  75. paintBrush.BeginBrushStroke(m_settings);
  76. for (size_t index = 0; index < locations.size(); index++)
  77. {
  78. EXPECT_CALL(mockHandler, OnSmooth(::testing::_, ::testing::_, ::testing::_, ::testing::_, ::testing::_)).Times(1);
  79. ON_CALL(mockHandler, OnSmooth)
  80. .WillByDefault(
  81. [=]([[maybe_unused]] const AZ::Color& color,
  82. const AZ::Aabb& dirtyArea,
  83. AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn,
  84. [[maybe_unused]] AZStd::span<const AZ::Vector3> valuePointOffsets,
  85. [[maybe_unused]] AzFramework::PaintBrushNotifications::SmoothFn& smoothFn)
  86. {
  87. validationFns[index](dirtyArea, valueLookupFn);
  88. });
  89. paintBrush.SmoothToLocation(locations[index], m_settings);
  90. }
  91. paintBrush.EndBrushStroke();
  92. paintBrush.EndPaintMode();
  93. }
  94. // Test out the blendFn that we're provided from the requested blend mode by running through sets of values and blending them.
  95. // The caller needs to provide a verifyFn that should produce an expected value that we'll compare against.
  96. void TestBlendModeForPaintAndSmooth(
  97. AzFramework::PaintBrushBlendMode blendMode,
  98. AZStd::function<float(float baseValue, float newValue, float opacity)> verifyFn)
  99. {
  100. AzFramework::PaintBrush paintBrush(EntityComponentIdPair);
  101. ::testing::NiceMock<MockPaintBrushNotificationBusHandler> mockHandler(EntityComponentIdPair);
  102. // Set the smooth mode to "Mean" so that we can fill the kernel values with all of the same values, which
  103. // lets us use the same verification function for testing the blendFn and the smoothFn since we'll have the
  104. // same baseValue, newValue, and opacity.
  105. m_settings.SetSmoothMode(AzFramework::PaintBrushSmoothMode::Mean);
  106. m_settings.SetBlendMode(blendMode);
  107. paintBrush.BeginPaintMode();
  108. // Test the blend mode with PaintToLocation()
  109. EXPECT_CALL(mockHandler, OnPaint(::testing::_, ::testing::_, ::testing::_, ::testing::_)).Times(1);
  110. ON_CALL(mockHandler, OnPaint)
  111. .WillByDefault(
  112. [=]([[maybe_unused]] const AZ::Color& color,
  113. [[maybe_unused]] const AZ::Aabb& dirtyArea,
  114. [[maybe_unused]] AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn,
  115. AzFramework::PaintBrushNotifications::BlendFn& blendFn)
  116. {
  117. for (float baseValue = 0.0f; baseValue <= 1.0f; baseValue += 0.25f)
  118. {
  119. for (float newValue = 0.0f; newValue <= 1.0f; newValue += 0.25f)
  120. {
  121. for (float opacity = 0.0f; opacity < 1.0f; opacity += 0.25f)
  122. {
  123. float expectedValue = AZStd::clamp(verifyFn(baseValue, newValue, opacity), 0.0f, 1.0f);
  124. EXPECT_NEAR(blendFn(baseValue, newValue, opacity), expectedValue, 0.001f);
  125. }
  126. }
  127. }
  128. });
  129. paintBrush.BeginBrushStroke(m_settings);
  130. paintBrush.PaintToLocation(TestBrushCenter, m_settings);
  131. paintBrush.EndBrushStroke();
  132. // Test the blend mode with SmoothToLocation()
  133. EXPECT_CALL(mockHandler, OnSmooth(::testing::_, ::testing::_, ::testing::_, ::testing::_, ::testing::_)).Times(1);
  134. ON_CALL(mockHandler, OnSmooth)
  135. .WillByDefault(
  136. [=]([[maybe_unused]] const AZ::Color& color,
  137. [[maybe_unused]] const AZ::Aabb& dirtyArea,
  138. [[maybe_unused]] AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn,
  139. [[maybe_unused]] AZStd::span<const AZ::Vector3> valuePointOffsets,
  140. AzFramework::PaintBrushNotifications::SmoothFn& smoothFn)
  141. {
  142. for (float baseValue = 0.0f; baseValue <= 1.0f; baseValue += 0.25f)
  143. {
  144. for (float newValue = 0.0f; newValue <= 1.0f; newValue += 0.25f)
  145. {
  146. // Create a 3x3 set of kernelValues all with newValue. The mean of this will be newValue,
  147. // so the output of smoothFn should be the same as blendFn for the same combinations of values.
  148. AZStd::vector<float> kernelValues(9, newValue);
  149. for (float opacity = 0.0f; opacity < 1.0f; opacity += 0.25f)
  150. {
  151. float expectedValue = AZStd::clamp(verifyFn(baseValue, newValue, opacity), 0.0f, 1.0f);
  152. EXPECT_NEAR(smoothFn(baseValue, kernelValues, opacity), expectedValue, 0.001f);
  153. }
  154. }
  155. }
  156. });
  157. paintBrush.BeginBrushStroke(m_settings);
  158. paintBrush.SmoothToLocation(TestBrushCenter, m_settings);
  159. paintBrush.EndBrushStroke();
  160. paintBrush.EndPaintMode();
  161. }
  162. };
  163. TEST_F(PaintBrushPaintSettingsTestFixture, ZeroOpacityBrushSettingCausesNoNotifications)
  164. {
  165. // If the opacity is zero (transparent), OnPaint/OnSmooth will never get called because no points can get modified.
  166. m_settings.SetColor(AZ::Color(0.0f));
  167. TestZeroNotificationsForPaintAndSmooth();
  168. }
  169. TEST_F(PaintBrushPaintSettingsTestFixture, SizeBrushSettingAffectsPaintBrush)
  170. {
  171. // The PaintBrush 'Size' setting should affect the overall size of the paint brush circle that's being used to paint/smooth.
  172. AzFramework::PaintBrush paintBrush(EntityComponentIdPair);
  173. ::testing::NiceMock<MockPaintBrushNotificationBusHandler> mockHandler(EntityComponentIdPair);
  174. // Loop through a series of different brush radius sizes.
  175. for (auto& brushRadiusSize : {0.5f, 1.0f, 5.0f, 10.0f, 20.0f})
  176. {
  177. m_settings.SetSize(brushRadiusSize * 2.0f);
  178. ValidationFn validateFn = [this, brushRadiusSize](const AZ::Aabb& dirtyArea,
  179. AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn)
  180. {
  181. // The dirtyArea AABB should change size based on the current brush radius size that we're using.
  182. EXPECT_THAT(dirtyArea, IsClose(AZ::Aabb::CreateCenterRadius(TestBrushCenter2d, brushRadiusSize)));
  183. // Create a 3x3 square grid of points. Because our brush is a circle, we expect only the points along a + to be
  184. // returned as valid points. The corners of the square should fall outside the circle and not get returned.
  185. // Since we're scaling this based on the AABB, this should be checking the same relative points for each
  186. // brush radius.
  187. const AZStd::vector<AZ::Vector3> points = {
  188. AZ::Vector3(dirtyArea.GetMin().GetX(), dirtyArea.GetMin().GetY(), 0.0f),
  189. AZ::Vector3(dirtyArea.GetCenter().GetX(), dirtyArea.GetMin().GetY(), 0.0f),
  190. AZ::Vector3(dirtyArea.GetMax().GetX(), dirtyArea.GetMin().GetY(), 0.0f),
  191. AZ::Vector3(dirtyArea.GetMin().GetX(), dirtyArea.GetCenter().GetY(), 0.0f),
  192. AZ::Vector3(dirtyArea.GetCenter().GetX(), dirtyArea.GetCenter().GetY(), 0.0f),
  193. AZ::Vector3(dirtyArea.GetMax().GetX(), dirtyArea.GetCenter().GetY(), 0.0f),
  194. AZ::Vector3(dirtyArea.GetMin().GetX(), dirtyArea.GetMax().GetY(), 0.0f),
  195. AZ::Vector3(dirtyArea.GetCenter().GetX(), dirtyArea.GetMax().GetY(), 0.0f),
  196. AZ::Vector3(dirtyArea.GetMax().GetX(), dirtyArea.GetMax().GetY(), 0.0f),
  197. };
  198. AZStd::vector<AZ::Vector3> validPoints;
  199. AZStd::vector<float> opacities;
  200. valueLookupFn(points, validPoints, opacities);
  201. // We should only have the 5 points along the + in validPoints.
  202. const AZStd::vector<AZ::Vector3> expectedValidPoints = {
  203. AZ::Vector3(dirtyArea.GetCenter().GetX(), dirtyArea.GetMin().GetY(), 0.0f),
  204. AZ::Vector3(dirtyArea.GetMin().GetX(), dirtyArea.GetCenter().GetY(), 0.0f),
  205. AZ::Vector3(dirtyArea.GetCenter().GetX(), dirtyArea.GetCenter().GetY(), 0.0f),
  206. AZ::Vector3(dirtyArea.GetMax().GetX(), dirtyArea.GetCenter().GetY(), 0.0f),
  207. AZ::Vector3(dirtyArea.GetCenter().GetX(), dirtyArea.GetMax().GetY(), 0.0f),
  208. };
  209. EXPECT_THAT(validPoints, ::testing::Pointwise(ContainerIsClose(), expectedValidPoints));
  210. };
  211. ValidatePaintAndSmooth(paintBrush, mockHandler, { &TestBrushCenter, 1 }, { &validateFn, 1 });
  212. }
  213. }
  214. TEST_F(PaintBrushPaintSettingsTestFixture, ZeroSizeBrushSettingCausesNoNotifications)
  215. {
  216. // If the brush size is zero, OnPaint/OnSmooth will never get called because no points can get modified.
  217. m_settings.SetSize(0.0f);
  218. TestZeroNotificationsForPaintAndSmooth();
  219. }
  220. TEST_F(PaintBrushPaintSettingsTestFixture, HardnessBrushSettingAffectsPaintBrush)
  221. {
  222. // The 'Hardness %' setting should apply an opacity falloff curve. It starts at the (radius * hardness%) distance from the
  223. // center and ends at the radius distance from the center.
  224. AzFramework::PaintBrush paintBrush(EntityComponentIdPair);
  225. ::testing::NiceMock<MockPaintBrushNotificationBusHandler> mockHandler(EntityComponentIdPair);
  226. const float TestRadiusSize = 10.0f;
  227. m_settings.SetSize(TestRadiusSize * 2.0f);
  228. // Loop through a series of different hardness % values. We'll test 100% separately.
  229. for (auto& hardnessPercent : { 0.0f, 1.0f, 50.0f, 99.0f })
  230. {
  231. m_settings.SetHardnessPercent(hardnessPercent);
  232. ValidationFn validateFn =
  233. [this, hardnessPercent, TestRadiusSize]([[maybe_unused]] const AZ::Aabb& dirtyArea, AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn)
  234. {
  235. // The falloff function should start at the hardness percentage from the center.
  236. const float falloffStart = hardnessPercent / 100.0f;
  237. const AZStd::vector<AZ::Vector3> points = {
  238. // Test the opacity at the brush center. It should be 1.
  239. TestBrushCenter2d,
  240. // Test the opacity at the hardness percent (i.e. the start of the falloff). It should also be 1.
  241. TestBrushCenter2d + AZ::Vector3(TestRadiusSize * falloffStart, 0.0f, 0.0f),
  242. // Test the opacity halfway between the falloff and the edge.
  243. // The opacity should be 0.5, because even though it's a falloff curve, the curve hits the midpoint
  244. // of (0.5, 0.5).
  245. TestBrushCenter2d + AZ::Vector3(TestRadiusSize * (falloffStart + ((1.0f - falloffStart) / 2.0f)), 0.0f, 0.0f),
  246. // Test the opacity at the edge of the brush.
  247. TestBrushCenter2d + AZ::Vector3(TestRadiusSize, 0.0f, 0.0f),
  248. };
  249. AZStd::vector<AZ::Vector3> validPoints;
  250. AZStd::vector<float> opacities;
  251. valueLookupFn(points, validPoints, opacities);
  252. // Only the first 3 points should be valid, since the 4th should have an opacity of 0.
  253. EXPECT_EQ(validPoints.size(), 3);
  254. // The brush should have an opacity of 1.0 from the center to the hardness % along the radius.
  255. // The falloff curve should hit 50% between the start of the falloff and the end.
  256. // The end is 0%, which won't get reported as a valid point, because it's transparent.
  257. const AZStd::vector<float> expectedOpacities = { 1.0f, 1.0f, 0.5f };
  258. EXPECT_THAT(opacities, ::testing::Pointwise(::testing::FloatNear(0.001f), expectedOpacities));
  259. };
  260. ValidatePaintAndSmooth(paintBrush, mockHandler, { &TestBrushCenter, 1 }, { &validateFn, 1 });
  261. }
  262. }
  263. TEST_F(PaintBrushPaintSettingsTestFixture, FullHardnessBrushSettingHasNoFalloff)
  264. {
  265. // Verify that 100% Hardness on PaintBrushSettings means there is no falloff.
  266. AzFramework::PaintBrush paintBrush(EntityComponentIdPair);
  267. ::testing::NiceMock<MockPaintBrushNotificationBusHandler> mockHandler(EntityComponentIdPair);
  268. const float TestRadiusSize = 10.0f;
  269. m_settings.SetSize(TestRadiusSize * 2.0f);
  270. m_settings.SetHardnessPercent(100.0f);
  271. // Verify that paint/smooth uses the hardness percent correctly.
  272. ValidationFn validateFn =
  273. [this, TestRadiusSize]([[maybe_unused]] const AZ::Aabb& dirtyArea, AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn)
  274. {
  275. const AZStd::vector<AZ::Vector3> points = {
  276. // Test the opacity at the brush center + 0%, 25%, 50%, 75%, 100%
  277. TestBrushCenter2d,
  278. TestBrushCenter2d + AZ::Vector3(TestRadiusSize * 0.25f, 0.0f, 0.0f),
  279. TestBrushCenter2d + AZ::Vector3(TestRadiusSize * 0.50f, 0.0f, 0.0f),
  280. TestBrushCenter2d + AZ::Vector3(TestRadiusSize * 0.75f, 0.0f, 0.0f),
  281. TestBrushCenter2d + AZ::Vector3(TestRadiusSize * 1.00f, 0.0f, 0.0f),
  282. };
  283. AZStd::vector<AZ::Vector3> validPoints;
  284. AZStd::vector<float> opacities;
  285. valueLookupFn(points, validPoints, opacities);
  286. // All 5 points should have opacity of 1.0 when using a hardness of 100%.
  287. const AZStd::vector<float> expectedOpacities(5, 1.0f);
  288. EXPECT_THAT(opacities, ::testing::Pointwise(::testing::FloatNear(0.001f), expectedOpacities));
  289. };
  290. ValidatePaintAndSmooth(paintBrush, mockHandler, { &TestBrushCenter, 1 }, { &validateFn, 1 });
  291. }
  292. TEST_F(PaintBrushPaintSettingsTestFixture, FlowBrushSettingAffectsPaintBrush)
  293. {
  294. // The 'Flow %' setting affects the opacity of each paint circle.
  295. // The alpha value in the stroke color (stroke opacity) provides a constant opacity of every circle
  296. // in the stroke regardless of how much they overlap.
  297. // Flow % provides an opacity for each circle that will accumulate where they overlap. It's a non-linear accumulation,
  298. // because each usage of flow % will be applied to the distance between the current opacity and 1.0. For example,
  299. // for 10% flow starting at opacity=0:
  300. // opacity = 0.0 + (1 - 0.0) * 0.1 = 0.1
  301. // opacity = 0.1 + (1 - 0.1) * 0.1 = 0.19
  302. // opacity = 0.19 + (1 - 0.19) * 0.1 = 0.271
  303. // ...
  304. AzFramework::PaintBrush paintBrush(EntityComponentIdPair);
  305. ::testing::NiceMock<MockPaintBrushNotificationBusHandler> mockHandler(EntityComponentIdPair);
  306. const float TestRadiusSize = 10.0f;
  307. m_settings.SetSize(TestRadiusSize * 2.0f);
  308. const float TestFlowPercent = 10.0f;
  309. const float TestFlow = TestFlowPercent / 100.0f;
  310. m_settings.SetFlowPercent(TestFlowPercent);
  311. const float TestDistancePercent = 50.0f;
  312. m_settings.SetDistancePercent(TestDistancePercent);
  313. // The first location is an arbitrary point, and the second location
  314. // is one full brush circle to the right of the first one along the X axis.
  315. const AZ::Vector3 secondLocation = TestBrushCenter + AZ::Vector3(TestRadiusSize * 2.0f, 0.0f, 0.0f);
  316. AZStd::vector<AZ::Vector3> locations = { TestBrushCenter, secondLocation };
  317. // On the first PaintToLocation() call, we only have a single brush circle, so it should have a constant
  318. // opacity value that matches our flow percentage.
  319. ValidationFn validateFirstCallFn =
  320. [this, TestRadiusSize, TestFlow]([[maybe_unused]] const AZ::Aabb& dirtyArea, AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn)
  321. {
  322. AZStd::vector<AZ::Vector3> points;
  323. // Generate a series of points that span across the center of the circle.
  324. for (float x = -TestRadiusSize; x <= TestRadiusSize; x += 1.0f)
  325. {
  326. points.emplace_back(TestBrushCenter2d + AZ::Vector3(x, 0.0f, 0.0f));
  327. }
  328. AZStd::vector<AZ::Vector3> validPoints;
  329. AZStd::vector<float> opacities;
  330. valueLookupFn(points, validPoints, opacities);
  331. // Every point we submitted should be valid.
  332. EXPECT_EQ(validPoints.size(), points.size());
  333. EXPECT_EQ(opacities.size(), points.size());
  334. // For the initial brush circle, every point should have the same opacity, which is our flow %.
  335. for (auto& opacity : opacities)
  336. {
  337. EXPECT_NEAR(opacity, TestFlow, 0.001f);
  338. }
  339. };
  340. /*
  341. On the second PaintToLocationCall(), we're going to move exactly full brush circle away along the X axis.
  342. However, because our distance % is set to 50%, we'll get 2 overlapping circles - 'a' and 'b' in this diagram.
  343. (The first circle of '.' is from the first PaintToLocation and doesn't show up in this one)
  344. . . a a b b
  345. . a . b a b
  346. . a .b a b
  347. . a .b a b
  348. . a . b a b
  349. . . a a b b
  350. |-----|----|-----|
  351. -2r -r 0 r
  352. If the Flow % opacity is working correctly, we should end up with 10% opacity where the 'a' and 'b' circles are separate,
  353. and 19% opacity where the two circles overlap, because the accumulation isn't a straight addition.
  354. We're using 50% distance between the circles, which is equal to the brush radius.
  355. Since the location that we're painting to is the center of circle 'b', we expect that from that center point, along the X axis,
  356. (-2 * radius) to (-1 * radius) falls in circle 'a' only and should be 10%. (-1 * radius) to (0) should
  357. fall in both circles and be 19%. (0) to (1 * radius) falls in circle 'b' only and should be 10% again.
  358. */
  359. ValidationFn validateSecondCallFn =
  360. [=](const AZ::Aabb& dirtyArea, AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn)
  361. {
  362. AZStd::vector<AZ::Vector3> points;
  363. // Generate a series of points that span across the entire dirty area along the center of the circles.
  364. for (float x = dirtyArea.GetMin().GetX(); x <= dirtyArea.GetMax().GetX(); x += 0.25f)
  365. {
  366. points.emplace_back(AZ::Vector3(x, dirtyArea.GetCenter().GetY(), dirtyArea.GetCenter().GetZ()));
  367. }
  368. AZStd::vector<AZ::Vector3> validPoints;
  369. AZStd::vector<float> opacities;
  370. valueLookupFn(points, validPoints, opacities);
  371. // Every point we submitted should be valid.
  372. EXPECT_EQ(validPoints.size(), points.size());
  373. EXPECT_EQ(opacities.size(), points.size());
  374. for (size_t index = 0; index < validPoints.size(); index++)
  375. {
  376. float xLocation = (validPoints[index] - secondLocation).GetX();
  377. if (xLocation < (-TestRadiusSize))
  378. {
  379. // Opacities in [-2*radius, -1*radius) only fall in circle 'a' and should be 10%
  380. EXPECT_NEAR(opacities[index], TestFlow, 0.001f);
  381. }
  382. else if (xLocation <= 0.0f)
  383. {
  384. // Opacities in [-1*radius, 0] fall in circle 'a' and 'b' and should be 19%
  385. EXPECT_NEAR(opacities[index], TestFlow + ((1.0f - TestFlow) * TestFlow), 0.001f);
  386. }
  387. else
  388. {
  389. // Opacities in (0, radius] only fall in circle 'b' and should be 10%
  390. EXPECT_NEAR(opacities[index], TestFlow, 0.001f);
  391. }
  392. }
  393. };
  394. AZStd::vector<ValidationFn> validationFns = { validateFirstCallFn, validateSecondCallFn };
  395. ValidatePaintAndSmooth(paintBrush, mockHandler, locations, validationFns);
  396. }
  397. TEST_F(PaintBrushPaintSettingsTestFixture, ZeroFlowBrushSettingCausesNoNotifications)
  398. {
  399. // If the flow % is zero, OnPaint/OnSmooth will never get called because no points can get modified.
  400. m_settings.SetFlowPercent(0.0f);
  401. TestZeroNotificationsForPaintAndSmooth();
  402. }
  403. TEST_F(PaintBrushPaintSettingsTestFixture, DistanceBrushSettingAffectsPaintBrush)
  404. {
  405. // The 'Distance %' setting affects how far apart each paint circle is applied during a brush movement.
  406. // The % is in terms of the brush size, so 50% produces circles that overlap by 50%, 100% produces circles that
  407. // perfectly don't overlap, 200% produces circles with exactly one empty circle between each one, etc.
  408. AzFramework::PaintBrush paintBrush(EntityComponentIdPair);
  409. ::testing::NiceMock<MockPaintBrushNotificationBusHandler> mockHandler(EntityComponentIdPair);
  410. const float TestRadiusSize = 10.0f;
  411. m_settings.SetSize(TestRadiusSize * 2.0f);
  412. // Choose a second location that's sufficiently far away that we'll get multiple brush circles for each of our
  413. // chosen distance % values.
  414. AZStd::vector<AZ::Vector3> locations = { TestBrushCenter, TestBrushCenter + AZ::Vector3(TestRadiusSize * 10.0f, 0.0f, 0.0f) };
  415. for (auto& distancePercent : {1.0f, 10.0f, 50.0f, 100.0f, 300.0f})
  416. {
  417. m_settings.SetDistancePercent(distancePercent);
  418. // On the first *ToLocation() call, we only have a single brush circle, so it should have a constant
  419. // opacity value that matches our flow percentage.
  420. ValidationFn validateFirstCallFn =
  421. [this, TestRadiusSize](const AZ::Aabb& dirtyArea, [[maybe_unused]] AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn)
  422. {
  423. // On the first call, the dirtyArea AABB should match the size of the brush.
  424. EXPECT_THAT(dirtyArea, IsClose(AZ::Aabb::CreateCenterRadius(TestBrushCenter2d, TestRadiusSize)));
  425. };
  426. ValidationFn validateSecondCallFn =
  427. [this, TestRadiusSize, distancePercent](const AZ::Aabb& dirtyArea, [[maybe_unused]] AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn)
  428. {
  429. // On the second call, a number of brush circles will be applied based on the distance %. The first
  430. // brush circle in this call will start distance % further than the left edge of our initial circle.
  431. const float initialStartX = (TestBrushCenter2d.GetX() - TestRadiusSize);
  432. const float expectedStartX = initialStartX + (TestRadiusSize * 2.0f) * (distancePercent / 100.0f);
  433. EXPECT_NEAR(dirtyArea.GetMin().GetX(), expectedStartX, 0.001f);
  434. };
  435. AZStd::vector<ValidationFn> validationFns = { validateFirstCallFn, validateSecondCallFn };
  436. ValidatePaintAndSmooth(paintBrush, mockHandler, locations, validationFns);
  437. }
  438. }
  439. TEST_F(PaintBrushPaintSettingsTestFixture, ZeroDistanceBrushSettingCausesNoNotifications)
  440. {
  441. // If the distance % is zero, OnPaint/OnSmooth will never get called because no points can get modified.
  442. m_settings.SetDistancePercent(0.0f);
  443. TestZeroNotificationsForPaintAndSmooth();
  444. }
  445. TEST_F(PaintBrushPaintSettingsTestFixture, NormalBlendBrushSettingIsCorrect)
  446. {
  447. // The 'Normal' Blend brush setting is just a standard lerp.
  448. TestBlendModeForPaintAndSmooth(
  449. AzFramework::PaintBrushBlendMode::Normal,
  450. [](float baseValue, float newValue, float opacity) -> float
  451. {
  452. return AZStd::lerp(baseValue, newValue, opacity);
  453. });
  454. }
  455. TEST_F(PaintBrushPaintSettingsTestFixture, AddBlendBrushSettingIsCorrect)
  456. {
  457. // The 'Add' Blend brush setting lerps between the base and 'base + new'.
  458. // Note that we specifically do NOT expect it to clamp the add. This matches Photoshop's behavior,
  459. // but other paint programs vary in their choice here.
  460. TestBlendModeForPaintAndSmooth(
  461. AzFramework::PaintBrushBlendMode::Add,
  462. [](float baseValue, float newValue, float opacity) -> float
  463. {
  464. return AZStd::lerp(baseValue, baseValue + newValue, opacity);
  465. });
  466. }
  467. TEST_F(PaintBrushPaintSettingsTestFixture, SubtractBlendBrushSettingIsCorrect)
  468. {
  469. // The 'Subtract' Blend brush setting lerps between the base and 'base - new'
  470. // Note that we specifically do NOT expect it to clamp the subtract. This matches Photoshop's behavior,
  471. // but other paint programs vary in their choice here.
  472. TestBlendModeForPaintAndSmooth(
  473. AzFramework::PaintBrushBlendMode::Subtract,
  474. [](float baseValue, float newValue, float opacity) -> float
  475. {
  476. return AZStd::lerp(baseValue, baseValue - newValue, opacity);
  477. });
  478. }
  479. TEST_F(PaintBrushPaintSettingsTestFixture, MultiplyBlendBrushSettingIsCorrect)
  480. {
  481. // The 'Multiply' Blend brush setting lerps between the base and 'base * new'
  482. TestBlendModeForPaintAndSmooth(
  483. AzFramework::PaintBrushBlendMode::Multiply,
  484. [](float baseValue, float newValue, float opacity) -> float
  485. {
  486. return AZStd::lerp(baseValue, baseValue * newValue, opacity);
  487. });
  488. }
  489. TEST_F(PaintBrushPaintSettingsTestFixture, ScreenBlendBrushSettingIsCorrect)
  490. {
  491. // The 'Screen' Blend brush setting lerps between the base and '1 - (1 - base) * (1 - new)'
  492. TestBlendModeForPaintAndSmooth(
  493. AzFramework::PaintBrushBlendMode::Screen,
  494. [](float baseValue, float newValue, float opacity) -> float
  495. {
  496. return AZStd::lerp(baseValue, 1.0f - ((1.0f - baseValue) * (1.0f - newValue)), opacity);
  497. });
  498. }
  499. TEST_F(PaintBrushPaintSettingsTestFixture, DarkenBlendBrushSettingIsCorrect)
  500. {
  501. // The 'Darken' Blend brush setting lerps between the base and 'min(base, new)'
  502. TestBlendModeForPaintAndSmooth(
  503. AzFramework::PaintBrushBlendMode::Darken,
  504. [](float baseValue, float newValue, float opacity) -> float
  505. {
  506. return AZStd::lerp(baseValue, AZStd::min(baseValue, newValue), opacity);
  507. });
  508. }
  509. TEST_F(PaintBrushPaintSettingsTestFixture, LightenBlendBrushSettingIsCorrect)
  510. {
  511. // The 'Lighten' Blend brush setting lerps between the base and 'max(base, new)'
  512. TestBlendModeForPaintAndSmooth(
  513. AzFramework::PaintBrushBlendMode::Lighten,
  514. [](float baseValue, float newValue, float opacity) -> float
  515. {
  516. return AZStd::lerp(baseValue, AZStd::max(baseValue, newValue), opacity);
  517. });
  518. }
  519. TEST_F(PaintBrushPaintSettingsTestFixture, AverageBlendBrushSettingIsCorrect)
  520. {
  521. // The 'Average' Blend brush setting lerps between the base and '(base + new) / 2'
  522. TestBlendModeForPaintAndSmooth(
  523. AzFramework::PaintBrushBlendMode::Average,
  524. [](float baseValue, float newValue, float opacity) -> float
  525. {
  526. return AZStd::lerp(baseValue, (baseValue + newValue) / 2.0f, opacity);
  527. });
  528. }
  529. TEST_F(PaintBrushPaintSettingsTestFixture, OverlayBlendBrushSettingIsCorrect)
  530. {
  531. // The 'Overlay' Blend brush setting lerps between the base and the following:
  532. // if base >= 0.5 : (1 - (2 * (1 - base) * (1 - new)))
  533. // if base < 0.5 : 2 * base * new
  534. TestBlendModeForPaintAndSmooth(
  535. AzFramework::PaintBrushBlendMode::Overlay,
  536. [](float baseValue, float newValue, float opacity) -> float
  537. {
  538. if (baseValue >= 0.5f)
  539. {
  540. return AZStd::lerp(baseValue, (1.0f - (2.0f * (1.0f - baseValue) * (1.0f - newValue))), opacity);
  541. }
  542. return AZStd::lerp(baseValue, 2.0f * baseValue * newValue, opacity);
  543. });
  544. }
  545. TEST_F(PaintBrushPaintSettingsTestFixture, SmoothingRadiusSettingAffectsSmoothBrush)
  546. {
  547. // The 'Smoothing Radius' setting affects how many values are blended together to produce a smoothed result value.
  548. // The values should be an NxN square, where N = (radius * 2) + 1.
  549. // Radius 1 = 3x3 square. Radius 2 = 5x5 square. Radius 3 = 7x7 square. etc.
  550. AzFramework::PaintBrush paintBrush(EntityComponentIdPair);
  551. ::testing::NiceMock<MockPaintBrushNotificationBusHandler> mockHandler(EntityComponentIdPair);
  552. // Set the smoothing mode to "Mean" so that we have an easily-predictable result.
  553. m_settings.SetSmoothMode(AzFramework::PaintBrushSmoothMode::Mean);
  554. paintBrush.BeginPaintMode();
  555. for (int32_t radius = 1; radius <= 5; radius++)
  556. {
  557. m_settings.SetSmoothingRadius(radius);
  558. EXPECT_CALL(mockHandler, OnSmooth(::testing::_, ::testing::_, ::testing::_, ::testing::_, ::testing::_)).Times(1);
  559. ON_CALL(mockHandler, OnSmooth)
  560. .WillByDefault(
  561. [=]([[maybe_unused]] const AZ::Color& color,
  562. [[maybe_unused]] const AZ::Aabb& dirtyArea,
  563. [[maybe_unused]] AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn,
  564. AZStd::span<const AZ::Vector3> valuePointOffsets,
  565. AzFramework::PaintBrushNotifications::SmoothFn& smoothFn)
  566. {
  567. const size_t kernelSize1d = (radius * 2) + 1;
  568. const size_t expectedKernelSize = kernelSize1d * kernelSize1d;
  569. // We expect the number of point offsets to match the NxN square size caused by our radius setting.
  570. EXPECT_EQ(valuePointOffsets.size(), expectedKernelSize);
  571. // Verify that the actual offsets we've been given go from -radius to radius in each direction.
  572. size_t index = 0;
  573. for (float y = aznumeric_cast<float>(-radius); y <= aznumeric_cast<float>(radius); y++)
  574. {
  575. for (float x = aznumeric_cast<float>(-radius); x <= aznumeric_cast<float>(radius); x++)
  576. {
  577. EXPECT_THAT(valuePointOffsets[index], IsClose(AZ::Vector3(x, y, 0.0f)));
  578. index++;
  579. }
  580. }
  581. // Create a set of kernelValues that's NxN in size and all 0's except that last value, which is 1.
  582. // Since our smoothing mode is "Mean", we should get a smoothed value of 1 / (NxN) if all of the kernelValues
  583. // are used in smoothing.
  584. AZStd::vector<float> kernelValues(valuePointOffsets.size(), 0.0f);
  585. kernelValues.back() = 1.0f;
  586. const float expectedResult = 1.0f / valuePointOffsets.size();
  587. float smoothedValue = smoothFn(0.0f, kernelValues, 1.0f);
  588. EXPECT_NEAR(smoothedValue, expectedResult, 0.001f);
  589. });
  590. paintBrush.BeginBrushStroke(m_settings);
  591. paintBrush.SmoothToLocation(TestBrushCenter, m_settings);
  592. paintBrush.EndBrushStroke();
  593. }
  594. paintBrush.EndPaintMode();
  595. }
  596. TEST_F(PaintBrushPaintSettingsTestFixture, GaussianSmoothModeIsCorrect)
  597. {
  598. // Verify that the Gaussian Smoothing mode produces the expected results.
  599. AzFramework::PaintBrush paintBrush(EntityComponentIdPair);
  600. ::testing::NiceMock<MockPaintBrushNotificationBusHandler> mockHandler(EntityComponentIdPair);
  601. // Use Gaussian with a 3x3 matrix for easily-testable results.
  602. m_settings.SetSmoothMode(AzFramework::PaintBrushSmoothMode::Gaussian);
  603. m_settings.SetSmoothingRadius(1);
  604. paintBrush.BeginPaintMode();
  605. EXPECT_CALL(mockHandler, OnSmooth(::testing::_, ::testing::_, ::testing::_, ::testing::_, ::testing::_)).Times(1);
  606. ON_CALL(mockHandler, OnSmooth)
  607. .WillByDefault(
  608. [=]([[maybe_unused]] const AZ::Color& color,
  609. [[maybe_unused]] const AZ::Aabb& dirtyArea,
  610. [[maybe_unused]] AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn,
  611. [[maybe_unused]] AZStd::span<const AZ::Vector3> valuePointOffsets,
  612. AzFramework::PaintBrushNotifications::SmoothFn& smoothFn)
  613. {
  614. // It's a bit tricky to validate Gaussian smoothing without just recreating the Gaussian calculations,
  615. // so we'll use "golden values" that are the precomputed 3x3 Gaussian matrix with known-good values.
  616. AZStd::vector<float> expectedGaussianMatrix = {
  617. 0.0751136f, 0.1238414f, 0.0751136f,
  618. 0.1238414f, 0.2041799f, 0.1238414f,
  619. 0.0751136f, 0.1238414f, 0.0751136f,
  620. };
  621. // Loop through and try smoothing with all values set to 0 except for one.
  622. // The result should match each value in our Gaussian matrix.
  623. for (size_t index = 0; index < 9; index++)
  624. {
  625. AZStd::vector<float> kernelValues(9, 0.0f);
  626. kernelValues[index] = 1.0f;
  627. const float smoothedValue = smoothFn(0.0f, kernelValues, 1.0f);
  628. EXPECT_NEAR(smoothedValue, expectedGaussianMatrix[index], 0.001f);
  629. }
  630. });
  631. paintBrush.BeginBrushStroke(m_settings);
  632. paintBrush.SmoothToLocation(TestBrushCenter, m_settings);
  633. paintBrush.EndBrushStroke();
  634. paintBrush.EndPaintMode();
  635. }
  636. TEST_F(PaintBrushPaintSettingsTestFixture, MeanSmoothModeIsCorrect)
  637. {
  638. // Verify that the Mean Smoothing mode produces the expected results.
  639. AzFramework::PaintBrush paintBrush(EntityComponentIdPair);
  640. ::testing::NiceMock<MockPaintBrushNotificationBusHandler> mockHandler(EntityComponentIdPair);
  641. // Use Mean with a 3x3 matrix for easily-testable results.
  642. m_settings.SetSmoothMode(AzFramework::PaintBrushSmoothMode::Mean);
  643. m_settings.SetSmoothingRadius(1);
  644. paintBrush.BeginPaintMode();
  645. EXPECT_CALL(mockHandler, OnSmooth(::testing::_, ::testing::_, ::testing::_, ::testing::_, ::testing::_)).Times(1);
  646. ON_CALL(mockHandler, OnSmooth)
  647. .WillByDefault(
  648. [=]([[maybe_unused]] const AZ::Color& color,
  649. [[maybe_unused]] const AZ::Aabb& dirtyArea,
  650. [[maybe_unused]] AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn,
  651. [[maybe_unused]] AZStd::span<const AZ::Vector3> valuePointOffsets,
  652. AzFramework::PaintBrushNotifications::SmoothFn& smoothFn)
  653. {
  654. // Loop through and try smoothing with all values set to 0 except for one.
  655. // The result should always be 1/9, since we're averaging all 9 values.
  656. for (size_t index = 0; index < 9; index++)
  657. {
  658. const float expectedResult = 1.0f / 9.0f;
  659. AZStd::vector<float> kernelValues(9, 0.0f);
  660. kernelValues[index] = 1.0f;
  661. const float smoothedValue = smoothFn(0.0f, kernelValues, 1.0f);
  662. EXPECT_NEAR(smoothedValue, expectedResult, 0.001f);
  663. }
  664. });
  665. paintBrush.BeginBrushStroke(m_settings);
  666. paintBrush.SmoothToLocation(TestBrushCenter, m_settings);
  667. paintBrush.EndBrushStroke();
  668. paintBrush.EndPaintMode();
  669. }
  670. TEST_F(PaintBrushPaintSettingsTestFixture, MedianSmoothModeIsCorrect)
  671. {
  672. // Verify that the Median Smoothing mode produces the expected results.
  673. AzFramework::PaintBrush paintBrush(EntityComponentIdPair);
  674. ::testing::NiceMock<MockPaintBrushNotificationBusHandler> mockHandler(EntityComponentIdPair);
  675. // Use Median with a 3x3 matrix for easily-testable results.
  676. m_settings.SetSmoothMode(AzFramework::PaintBrushSmoothMode::Median);
  677. m_settings.SetSmoothingRadius(1);
  678. paintBrush.BeginPaintMode();
  679. EXPECT_CALL(mockHandler, OnSmooth(::testing::_, ::testing::_, ::testing::_, ::testing::_, ::testing::_)).Times(1);
  680. ON_CALL(mockHandler, OnSmooth)
  681. .WillByDefault(
  682. [=]([[maybe_unused]] const AZ::Color& color, [[maybe_unused]] const AZ::Aabb& dirtyArea,
  683. [[maybe_unused]] AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn,
  684. [[maybe_unused]] AZStd::span<const AZ::Vector3> valuePointOffsets,
  685. AzFramework::PaintBrushNotifications::SmoothFn& smoothFn)
  686. {
  687. // Set our kernel values to 0.0, 0.01, 0.02, 0.03, 0.04, 0.5, 0.6, 0.7, 0.8 in scrambled order.
  688. // The middle value should be 0.04. These values are non-linear to ensure that we're
  689. // not taking the average of the values, and 0.04 is not the center value to ensure
  690. // that we're still finding it correctly.
  691. AZStd::vector<float> kernelValues = { 0.03f, 0.04f, 0.8f, 0.01f, 0.6f, 0.5f, 0.7f, 0.0f, 0.02f };
  692. const float expectedResult = 0.04f;
  693. const float smoothedValue = smoothFn(0.0f, kernelValues, 1.0f);
  694. EXPECT_NEAR(smoothedValue, expectedResult, 0.001f);
  695. });
  696. paintBrush.BeginBrushStroke(m_settings);
  697. paintBrush.SmoothToLocation(TestBrushCenter, m_settings);
  698. paintBrush.EndBrushStroke();
  699. paintBrush.EndPaintMode();
  700. }
  701. } // namespace UnitTest