MacroMaterialImageModification.cpp 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  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 <TerrainRenderer/Components/MacroMaterialImageModification.h>
  9. #include <LmbrCentral/Dependency/DependencyNotificationBus.h>
  10. #include <TerrainSystem/TerrainSystemBus.h>
  11. namespace Terrain
  12. {
  13. ImageTileBuffer::ImageTileBuffer(uint32_t imageWidth, uint32_t imageHeight, AZ::EntityId modifiedEntityId)
  14. : m_modifiedEntityId(modifiedEntityId)
  15. // Calculate the number of image tiles in each direction that we'll need, rounding up so that we create an image tile
  16. // for fractional tiles as well.
  17. , m_numTilesX((imageWidth + ImageTileSize - 1) / ImageTileSize)
  18. , m_numTilesY((imageHeight + ImageTileSize - 1) / ImageTileSize)
  19. {
  20. // Create empty entries for every tile. Each entry is just a null pointer at the start, so the memory overhead
  21. // of these empty entries at 32x32 pixels per tile, a 1024x1024 image will have 8 KB of overhead.
  22. m_paintedImageTiles.resize(m_numTilesX * m_numTilesY);
  23. }
  24. bool ImageTileBuffer::Empty() const
  25. {
  26. return !m_modifiedAnyPixels;
  27. }
  28. AZStd::pair<AZ::Color, float> ImageTileBuffer::GetOriginalPixelValueAndOpacity(const PixelIndex& pixelIndex)
  29. {
  30. uint32_t tileIndex = GetTileIndex(pixelIndex);
  31. uint32_t pixelTileIndex = GetPixelTileIndex(pixelIndex);
  32. // Create the tile if it doesn't already exist.
  33. // We lazy-create the tile on reads as well as writes because reading the original pixel value isn't necessarily very cheap
  34. // and we may need to re-read the same pixel multiple times for things like smoothing operations.
  35. CreateImageTile(tileIndex);
  36. return { m_paintedImageTiles[tileIndex]->m_unmodifiedData[pixelTileIndex],
  37. m_paintedImageTiles[tileIndex]->m_modifiedDataOpacity[pixelTileIndex] };
  38. }
  39. void ImageTileBuffer::SetModifiedPixelValue(const PixelIndex& pixelIndex, AZ::Color modifiedValue, float opacity)
  40. {
  41. uint32_t tileIndex = GetTileIndex(pixelIndex);
  42. uint32_t pixelTileIndex = GetPixelTileIndex(pixelIndex);
  43. // Create the tile if it doesn't already exist.
  44. CreateImageTile(tileIndex);
  45. m_paintedImageTiles[tileIndex]->m_modifiedData[pixelTileIndex] = modifiedValue;
  46. m_paintedImageTiles[tileIndex]->m_modifiedDataOpacity[pixelTileIndex] = opacity;
  47. }
  48. void ImageTileBuffer::ApplyChangeBuffer(bool undo)
  49. {
  50. AZStd::array<PixelIndex, ImageTileSize * ImageTileSize> pixelIndices;
  51. TerrainMacroColorModificationBus::Event(
  52. m_modifiedEntityId, &TerrainMacroColorModificationBus::Events::StartMacroColorPixelModifications);
  53. for (int32_t tileIndex = 0; tileIndex < m_paintedImageTiles.size(); tileIndex++)
  54. {
  55. // If we never created this tile, skip it and move on.
  56. if (m_paintedImageTiles[tileIndex] == nullptr)
  57. {
  58. continue;
  59. }
  60. // Create an array of pixel indices for every pixel in this tile.
  61. PixelIndex startIndex = GetStartPixelIndex(tileIndex);
  62. uint32_t index = 0;
  63. for (int16_t y = 0; y < ImageTileSize; y++)
  64. {
  65. for (int16_t x = 0; x < ImageTileSize; x++)
  66. {
  67. pixelIndices[index++] =
  68. PixelIndex(aznumeric_cast<int16_t>(startIndex.first + x), aznumeric_cast<int16_t>(startIndex.second + y));
  69. }
  70. }
  71. // Set the pixel values for this tile either to the original or the modified values.
  72. // It's possible that not every pixel in the tile was modified, but it's cheaper just to update per-tile
  73. // than to track each individual pixel in the tile and set them individually.
  74. TerrainMacroColorModificationBus::Event(
  75. m_modifiedEntityId,
  76. &TerrainMacroColorModificationBus::Events::SetMacroColorPixelValuesByPixelIndex,
  77. pixelIndices,
  78. undo ? m_paintedImageTiles[tileIndex]->m_unmodifiedData : m_paintedImageTiles[tileIndex]->m_modifiedData);
  79. }
  80. TerrainMacroColorModificationBus::Event(
  81. m_modifiedEntityId, &TerrainMacroColorModificationBus::Events::EndMacroColorPixelModifications);
  82. }
  83. uint32_t ImageTileBuffer::GetTileIndex(const PixelIndex& pixelIndex) const
  84. {
  85. return ((pixelIndex.second / ImageTileSize) * m_numTilesX) + (pixelIndex.first / ImageTileSize);
  86. }
  87. PixelIndex ImageTileBuffer::GetStartPixelIndex(uint32_t tileIndex) const
  88. {
  89. return PixelIndex(
  90. aznumeric_cast<int16_t>((tileIndex % m_numTilesX) * ImageTileSize),
  91. aznumeric_cast<int16_t>((tileIndex / m_numTilesX) * ImageTileSize));
  92. }
  93. uint32_t ImageTileBuffer::GetPixelTileIndex(const PixelIndex& pixelIndex) const
  94. {
  95. uint32_t xIndex = pixelIndex.first % ImageTileSize;
  96. uint32_t yIndex = pixelIndex.second % ImageTileSize;
  97. return (yIndex * ImageTileSize) + xIndex;
  98. }
  99. void ImageTileBuffer::CreateImageTile(uint32_t tileIndex)
  100. {
  101. // If it already exists, there's nothing more to do.
  102. if (m_paintedImageTiles[tileIndex])
  103. {
  104. return;
  105. }
  106. auto imageTile = AZStd::make_unique<ImageTile>();
  107. // Initialize the list of pixel indices for this tile.
  108. AZStd::array<PixelIndex, ImageTileSize * ImageTileSize> pixelIndices;
  109. PixelIndex startIndex = GetStartPixelIndex(tileIndex);
  110. for (int16_t index = 0; index < (ImageTileSize * ImageTileSize); index++)
  111. {
  112. pixelIndices[index] = PixelIndex(
  113. aznumeric_cast<int16_t>(startIndex.first + (index % ImageTileSize)),
  114. aznumeric_cast<int16_t>(startIndex.second + (index / ImageTileSize)));
  115. }
  116. AZ_Assert(imageTile->m_unmodifiedData.size() == pixelIndices.size(), "ImageTile and PixelIndices are out of sync.");
  117. // Read all of the original pixel values into the image tile buffer.
  118. TerrainMacroColorModificationBus::Event(
  119. m_modifiedEntityId,
  120. &TerrainMacroColorModificationBus::Events::GetMacroColorPixelValuesByPixelIndex,
  121. pixelIndices,
  122. imageTile->m_unmodifiedData);
  123. // Initialize the modified value buffer with the original values. This way we can always undo/redo an entire tile at a time
  124. // without tracking which pixels in the tile have been modified.
  125. imageTile->m_modifiedData = imageTile->m_unmodifiedData;
  126. AZStd::fill(imageTile->m_modifiedDataOpacity.begin(), imageTile->m_modifiedDataOpacity.end(), 0.0f);
  127. m_paintedImageTiles[tileIndex] = AZStd::move(imageTile);
  128. // If we create a tile, we'll use that as shorthand for tracking that changed data exists.
  129. m_modifiedAnyPixels = true;
  130. }
  131. ModifiedImageRegion::ModifiedImageRegion(const MacroMaterialImageSizeData& imageData)
  132. : m_minModifiedPixelIndex(AZStd::numeric_limits<int16_t>::max(), AZStd::numeric_limits<int16_t>::max())
  133. , m_maxModifiedPixelIndex(aznumeric_cast<int16_t>(-1), aznumeric_cast<int16_t>(-1))
  134. , m_isModified(false)
  135. , m_imageData(imageData)
  136. {
  137. }
  138. void ModifiedImageRegion::AddPoint(const PixelIndex& pixelIndex)
  139. {
  140. // Each time we modify a pixel, adjust our min and max pixel ranges to include it.
  141. m_minModifiedPixelIndex = PixelIndex(
  142. AZStd::min(m_minModifiedPixelIndex.first, pixelIndex.first), AZStd::min(m_minModifiedPixelIndex.second, pixelIndex.second));
  143. m_maxModifiedPixelIndex = PixelIndex(
  144. AZStd::max(m_maxModifiedPixelIndex.first, pixelIndex.first), AZStd::max(m_maxModifiedPixelIndex.second, pixelIndex.second));
  145. // Track that we've modified at least one pixel.
  146. m_isModified = true;
  147. }
  148. void ModifiedImageRegion::AddPixelAabb(const MacroMaterialImageSizeData& imageData, int16_t pixelX, int16_t pixelY, AZ::Aabb& region)
  149. {
  150. // This adds an AABB representing the size of one pixel in local space.
  151. // This method calculates the pixel's location from the top left corner of the local bounds.
  152. // Get the local bounds of the image gradient.
  153. const AZ::Aabb localBounds = imageData.m_macroMaterialBounds;
  154. int16_t shiftedPixelX = pixelX;
  155. int16_t shiftedPixelY = pixelY;
  156. // X pixels run left to right (min to max), but Y pixels run top to bottom (max to min), so we account for that
  157. // in the math below (it's why we do "min X +" and "max Y -").
  158. region.AddPoint(AZ::Vector3(
  159. localBounds.GetMin().GetX() + (imageData.m_metersPerPixelX * shiftedPixelX),
  160. localBounds.GetMax().GetY() - (imageData.m_metersPerPixelY * shiftedPixelY),
  161. 0.0f));
  162. region.AddPoint(AZ::Vector3(
  163. localBounds.GetMin().GetX() + (imageData.m_metersPerPixelX * (shiftedPixelX + 1)),
  164. localBounds.GetMax().GetY() - (imageData.m_metersPerPixelY * shiftedPixelY),
  165. 0.0f));
  166. region.AddPoint(AZ::Vector3(
  167. localBounds.GetMin().GetX() + (imageData.m_metersPerPixelX * shiftedPixelX),
  168. localBounds.GetMax().GetY() - (imageData.m_metersPerPixelY * (shiftedPixelY + 1)),
  169. 0.0f));
  170. region.AddPoint(AZ::Vector3(
  171. localBounds.GetMin().GetX() + (imageData.m_metersPerPixelX * (shiftedPixelX + 1)),
  172. localBounds.GetMax().GetY() - (imageData.m_metersPerPixelY * (shiftedPixelY + 1)),
  173. 0.0f));
  174. };
  175. AZ::Aabb ModifiedImageRegion::GetDirtyRegion()
  176. {
  177. // If the image hasn't been modified, return an invalid/unbounded dirty region.
  178. if (!m_isModified)
  179. {
  180. return AZ::Aabb::CreateNull();
  181. }
  182. // Create an AABB for our modified region based on the min/max pixels that were modified
  183. AZ::Aabb modifiedRegion = AZ::Aabb::CreateNull();
  184. AddPixelAabb(m_imageData, m_minModifiedPixelIndex.first, m_minModifiedPixelIndex.second, modifiedRegion);
  185. AddPixelAabb(m_imageData, m_maxModifiedPixelIndex.first, m_maxModifiedPixelIndex.second, modifiedRegion);
  186. // Because macro color textures use bilinear filtering, expand the dirty area by an extra pixel in each direction
  187. // so that the effects of the painted values on adjacent pixels are taken into account when refreshing.
  188. modifiedRegion.Expand(AZ::Vector3(m_imageData.m_metersPerPixelX, m_imageData.m_metersPerPixelY, 0.0f));
  189. // Finally, set the region to encompass the full Z range since macro materials are effectively 2D.
  190. modifiedRegion.Set(
  191. AZ::Vector3(modifiedRegion.GetMin().GetX(), modifiedRegion.GetMin().GetY(), AZStd::numeric_limits<float>::lowest()),
  192. AZ::Vector3(modifiedRegion.GetMax().GetX(), modifiedRegion.GetMax().GetY(), AZStd::numeric_limits<float>::max()));
  193. return modifiedRegion;
  194. }
  195. MacroMaterialImageModifier::MacroMaterialImageModifier(
  196. const AZ::EntityComponentIdPair& entityComponentIdPair)
  197. : m_ownerEntityComponentId(entityComponentIdPair)
  198. {
  199. AzFramework::PaintBrushNotificationBus::Handler::BusConnect(entityComponentIdPair);
  200. auto entityId = entityComponentIdPair.GetEntityId();
  201. // Get the spacing to map individual pixels to world space positions.
  202. AZ::Vector2 imagePixelsPerMeter(0.0f);
  203. TerrainMacroMaterialRequestBus::EventResult(
  204. imagePixelsPerMeter, entityId, &TerrainMacroMaterialRequestBus::Events::GetMacroColorImagePixelsPerMeter);
  205. // Convert from pixels per meter to meters per pixel so that we can guard for division by 0 here instead of everywhere.
  206. m_imageData.m_metersPerPixelX = (imagePixelsPerMeter.GetX() > 0.0f) ? (1.0f / imagePixelsPerMeter.GetX()) : 0.0f;
  207. m_imageData.m_metersPerPixelY = (imagePixelsPerMeter.GetY() > 0.0f) ? (1.0f / imagePixelsPerMeter.GetY()) : 0.0f;
  208. // Get the macro material world bounds.
  209. MacroMaterialData macroMaterialData;
  210. TerrainMacroMaterialRequestBus::EventResult(
  211. macroMaterialData, entityId, &TerrainMacroMaterialRequestBus::Events::GetTerrainMacroMaterialData);
  212. m_imageData.m_macroMaterialBounds = macroMaterialData.m_bounds;
  213. // Get the image width and height in pixels.
  214. AZ::RHI::Size imageSize;
  215. TerrainMacroMaterialRequestBus::EventResult(
  216. imageSize, entityId, &TerrainMacroMaterialRequestBus::Events::GetMacroColorImageSize);
  217. m_imageData.m_imageWidth = aznumeric_cast<int16_t>(imageSize.m_width);
  218. m_imageData.m_imageHeight = aznumeric_cast<int16_t>(imageSize.m_height);
  219. }
  220. MacroMaterialImageModifier::~MacroMaterialImageModifier()
  221. {
  222. AzFramework::PaintBrushNotificationBus::Handler::BusDisconnect();
  223. }
  224. void MacroMaterialImageModifier::OnBrushStrokeBegin([[maybe_unused]] const AZ::Color& color)
  225. {
  226. AZ::EntityId entityId = m_ownerEntityComponentId.GetEntityId();
  227. TerrainMacroColorModificationNotificationBus::Event(
  228. entityId, &TerrainMacroColorModificationNotificationBus::Events::OnTerrainMacroColorBrushStrokeBegin);
  229. // We can't create a stroke buffer if there isn't any pixel data.
  230. if ((m_imageData.m_imageWidth == 0) || (m_imageData.m_imageHeight == 0))
  231. {
  232. return;
  233. }
  234. // Create the buffer for holding all the changes for a single continuous paint brush stroke.
  235. // This buffer will get used during the stroke to hold our accumulated stroke opacity layer,
  236. // and then after the stroke finishes we'll hand the buffer over to the undo system as an undo/redo buffer.
  237. m_strokeBuffer =
  238. AZStd::make_shared<ImageTileBuffer>(m_imageData.m_imageWidth, m_imageData.m_imageHeight, entityId);
  239. m_modifiedStrokeRegion = ModifiedImageRegion(m_imageData);
  240. }
  241. void MacroMaterialImageModifier::OnBrushStrokeEnd()
  242. {
  243. const AZ::EntityId entityId = m_ownerEntityComponentId.GetEntityId();
  244. const AZ::Aabb dirtyRegion = m_modifiedStrokeRegion.GetDirtyRegion();
  245. TerrainMacroColorModificationNotificationBus::Event(
  246. entityId,
  247. &TerrainMacroColorModificationNotificationBus::Events::OnTerrainMacroColorBrushStrokeEnd,
  248. m_strokeBuffer, dirtyRegion);
  249. // Make sure we've cleared out our paint stroke and dirty region data until the next paint stroke begins.
  250. m_strokeBuffer = {};
  251. m_modifiedStrokeRegion = {};
  252. }
  253. AZ::Color MacroMaterialImageModifier::OnGetColor(const AZ::Vector3& brushCenter) const
  254. {
  255. AZ::EntityId entityId = m_ownerEntityComponentId.GetEntityId();
  256. AZ::Color color(0.0f, 0.0f, 0.0f, 1.0f);
  257. TerrainMacroColorModificationBus::Event(
  258. entityId,
  259. &TerrainMacroColorModificationBus::Events::GetMacroColorPixelValuesByPosition,
  260. AZStd::span<const AZ::Vector3>(&brushCenter, 1),
  261. AZStd::span<AZ::Color>(&color, 1));
  262. return color;
  263. }
  264. void MacroMaterialImageModifier::OnPaintSmoothInternal(
  265. const AZ::Aabb& dirtyArea,
  266. ValueLookupFn& valueLookupFn,
  267. AZStd::function<AZ::Color(const AZ::Vector3& worldPosition, AZ::Color gradientValue, float opacity)> combineFn)
  268. {
  269. ModifiedImageRegion modifiedRegion(m_imageData);
  270. // We're either painting or smoothing new values into our macro material.
  271. // To do this, we need to calculate the set of world space positions that map to individual pixels in the image,
  272. // then ask the paint brush for each position what value we should set that pixel to. Finally, we use those modified
  273. // values to change the macro material.
  274. const int32_t xPoints = aznumeric_cast<int32_t>(AZStd::round(dirtyArea.GetXExtent() / m_imageData.m_metersPerPixelX));
  275. const int32_t yPoints = aznumeric_cast<int32_t>(AZStd::round(dirtyArea.GetYExtent() / m_imageData.m_metersPerPixelY));
  276. // Early out if the dirty area is smaller than our point size.
  277. if ((xPoints <= 0) || (yPoints <= 0))
  278. {
  279. return;
  280. }
  281. // Calculate the minimum set of world space points that map to those pixels.
  282. AZStd::vector<AZ::Vector3> points;
  283. points.reserve(xPoints * yPoints);
  284. for (float y = dirtyArea.GetMin().GetY() + (m_imageData.m_metersPerPixelY / 2.0f);
  285. y <= dirtyArea.GetMax().GetY();
  286. y += m_imageData.m_metersPerPixelY)
  287. {
  288. for (float x = dirtyArea.GetMin().GetX() + (m_imageData.m_metersPerPixelX / 2.0f);
  289. x <= dirtyArea.GetMax().GetX();
  290. x += m_imageData.m_metersPerPixelX)
  291. {
  292. points.emplace_back(x, y, dirtyArea.GetMin().GetZ());
  293. }
  294. }
  295. // Query the paintbrush with those points to get back the subset of points and brush opacities for each point that's
  296. // affected by the brush.
  297. AZStd::vector<AZ::Vector3> validPoints;
  298. AZStd::vector<float> perPixelOpacities;
  299. valueLookupFn(points, validPoints, perPixelOpacities);
  300. // Early out if none of the points were actually affected by the brush.
  301. if (validPoints.empty())
  302. {
  303. return;
  304. }
  305. AZ::EntityId entityId = m_ownerEntityComponentId.GetEntityId();
  306. // Get the pixel indices for each position.
  307. AZStd::vector<PixelIndex> pixelIndices(validPoints.size());
  308. TerrainMacroColorModificationBus::Event(
  309. entityId, &TerrainMacroColorModificationBus::Events::GetMacroColorPixelIndicesForPositions, validPoints, pixelIndices);
  310. // Create a buffer for all of the modified, blended pixel values.
  311. AZStd::vector<AZ::Color> paintedValues;
  312. paintedValues.reserve(pixelIndices.size());
  313. // For each pixel, accumulate the per-pixel opacity in the stroke layer, then (re)blend the stroke layer with
  314. // the original data by using the stroke color, stroke opacity, per-pixel opacity, and original pre-stroke pixel color value.
  315. // The (re)blended value gets sent immediately to the macro material, as well as getting cached off into the stroke buffer
  316. // for easier and faster undo/redo operations.
  317. for (size_t index = 0; index < pixelIndices.size(); index++)
  318. {
  319. // If we have an invalid pixel index, fill in a placeholder value into paintedValues and move on to the next pixel.
  320. if ((pixelIndices[index].first < 0) || (pixelIndices[index].second < 0))
  321. {
  322. paintedValues.emplace_back(AZ::Color::CreateZero());
  323. continue;
  324. }
  325. auto [originalColor, opacityValue] = m_strokeBuffer->GetOriginalPixelValueAndOpacity(pixelIndices[index]);
  326. // Add the new per-pixel opacity to the existing opacity in our stroke layer.
  327. opacityValue = AZStd::clamp(opacityValue + (1.0f - opacityValue) * perPixelOpacities[index], 0.0f, 1.0f);
  328. // Blend the pixel and store the blended pixel and new opacity back into our paint stroke buffer.
  329. AZ::Color blendedColor = combineFn(validPoints[index], originalColor, opacityValue);
  330. m_strokeBuffer->SetModifiedPixelValue(pixelIndices[index], blendedColor, opacityValue);
  331. // Also store the blended value into a second buffer that we'll use to immediately modify the macro material.
  332. paintedValues.emplace_back(blendedColor);
  333. // Track the data needed for calculating the dirty region for this specific operation as well as for the overall brush stroke.
  334. modifiedRegion.AddPoint(pixelIndices[index]);
  335. m_modifiedStrokeRegion.AddPoint(pixelIndices[index]);
  336. }
  337. // Modify the macro material with all of the changed values
  338. TerrainMacroColorModificationBus::Event(entityId, &TerrainMacroColorModificationBus::Events::StartMacroColorPixelModifications);
  339. TerrainMacroColorModificationBus::Event(
  340. entityId, &TerrainMacroColorModificationBus::Events::SetMacroColorPixelValuesByPixelIndex, pixelIndices, paintedValues);
  341. TerrainMacroColorModificationBus::Event(entityId, &TerrainMacroColorModificationBus::Events::EndMacroColorPixelModifications);
  342. // Get the dirty region that actually encompasses everything that we directly modified,
  343. // along with everything it indirectly affected.
  344. if (modifiedRegion.IsModified())
  345. {
  346. AZ::Aabb expandedDirtyArea = modifiedRegion.GetDirtyRegion();
  347. // Notify the terrain system that the color data in this region has changed.
  348. // We don't need to notify anything else because the terrain renderer will automatically get the changes from the
  349. // uploaded texture changes.
  350. TerrainSystemServiceRequestBus::Broadcast(
  351. &TerrainSystemServiceRequestBus::Events::RefreshRegion,
  352. expandedDirtyArea,
  353. AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask::ColorData);
  354. }
  355. }
  356. void MacroMaterialImageModifier::OnPaint(
  357. const AZ::Color& color, const AZ::Aabb& dirtyArea, ValueLookupFn& valueLookupFn, BlendFn& blendFn)
  358. {
  359. // For paint notifications, we'll use the given blend function to blend the original value and the paint brush intensity
  360. // using the built-up opacity.
  361. auto combineFn = [color, blendFn](
  362. [[maybe_unused]] const AZ::Vector3& worldPosition, AZ::Color originalColor, float opacityValue) -> AZ::Color
  363. {
  364. // There's an optimization opportunity here by finding a way to rework the blendFn so that it can blend
  365. // multiple channels at once, instead of blending each channel separately.
  366. float red = blendFn(originalColor.GetR(), color.GetR(), opacityValue * color.GetA());
  367. float green = blendFn(originalColor.GetG(), color.GetG(), opacityValue * color.GetA());
  368. float blue = blendFn(originalColor.GetB(), color.GetB(), opacityValue * color.GetA());
  369. return AZ::Color(red, green, blue, originalColor.GetA());
  370. };
  371. // Perform all the common logic between painting and smoothing to modify our macro material.
  372. OnPaintSmoothInternal(dirtyArea, valueLookupFn, combineFn);
  373. }
  374. void MacroMaterialImageModifier::OnSmooth(
  375. const AZ::Color& color, const AZ::Aabb& dirtyArea,
  376. ValueLookupFn& valueLookupFn,
  377. AZStd::span<const AZ::Vector3> valuePointOffsets,
  378. SmoothFn& smoothFn)
  379. {
  380. AZ::EntityId entityId = m_ownerEntityComponentId.GetEntityId();
  381. // Declare our vectors of kernel point locations and values once outside of the combine function so that we
  382. // don't keep reallocating them on every point.
  383. AZStd::vector<AZ::Vector3> kernelPoints;
  384. AZStd::vector<AZ::Color> kernelValues;
  385. const AZ::Vector3 valuePointOffsetScale(m_imageData.m_metersPerPixelX, m_imageData.m_metersPerPixelY, 0.0f);
  386. kernelPoints.reserve(valuePointOffsets.size());
  387. kernelValues.reserve(valuePointOffsets.size());
  388. // For smoothing notifications, we'll need to gather all of the neighboring macro material values to feed into the given smoothing
  389. // function for our blend operation.
  390. auto combineFn = [entityId, color, smoothFn, &valuePointOffsets, valuePointOffsetScale, &kernelPoints, &kernelValues](
  391. const AZ::Vector3& worldPosition, AZ::Color originalColor, float opacityValue) -> AZ::Color
  392. {
  393. kernelPoints.clear();
  394. // Calculate all of the world positions around our base position that we'll use for fetching our blurring kernel values.
  395. for (auto& valuePointOffset : valuePointOffsets)
  396. {
  397. kernelPoints.emplace_back(worldPosition + (valuePointOffset * valuePointOffsetScale));
  398. }
  399. kernelValues.assign(kernelPoints.size(), AZ::Color::CreateZero());
  400. // Read all of the original macro color values for the blurring kernel into the buffer.
  401. AZStd::vector<PixelIndex> pixelIndices(kernelPoints.size());
  402. TerrainMacroColorModificationBus::Event(
  403. entityId,
  404. &TerrainMacroColorModificationBus::Events::GetMacroColorPixelValuesByPosition,
  405. kernelPoints, kernelValues);
  406. AZStd::vector<float> kernelValuesSingleChannel;
  407. // Blend each color channel separately.
  408. // Eventually it would be nice to refactor this so that the paint and smooth functions could take in multiple channels of data.
  409. for (auto& value : kernelValues)
  410. {
  411. kernelValuesSingleChannel.push_back(value.GetR());
  412. }
  413. float red = smoothFn(originalColor.GetR(), kernelValuesSingleChannel, opacityValue * color.GetA());
  414. kernelValuesSingleChannel.clear();
  415. for (auto& value : kernelValues)
  416. {
  417. kernelValuesSingleChannel.push_back(value.GetG());
  418. }
  419. float green = smoothFn(originalColor.GetG(), kernelValuesSingleChannel, opacityValue * color.GetA());
  420. kernelValuesSingleChannel.clear();
  421. for (auto& value : kernelValues)
  422. {
  423. kernelValuesSingleChannel.push_back(value.GetB());
  424. }
  425. float blue = smoothFn(originalColor.GetB(), kernelValuesSingleChannel, opacityValue * color.GetA());
  426. // Blend all the blurring kernel values together and store the blended pixel and new opacity back into our paint stroke buffer.
  427. return AZ::Color(red, green, blue, originalColor.GetA());
  428. };
  429. // Perform all the common logic between painting and smoothing to modify our macro material.
  430. OnPaintSmoothInternal(dirtyArea, valueLookupFn, combineFn);
  431. }
  432. } // namespace Terrain