1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321 |
- /*
- * Copyright (c) Contributors to the Open 3D Engine Project.
- * For complete copyright and license terms please see the LICENSE at the root of this distribution.
- *
- * SPDX-License-Identifier: Apache-2.0 OR MIT
- *
- */
- #include <GradientSignal/Components/ImageGradientComponent.h>
- #include <Atom/ImageProcessing/ImageProcessingDefines.h>
- #include <Atom/RPI.Public/RPIUtils.h>
- #include <AzCore/Asset/AssetManager.h>
- #include <AzCore/Asset/AssetSerializer.h>
- #include <AzCore/Debug/Profiler.h>
- #include <AzCore/Math/MathUtils.h>
- #include <AzCore/RTTI/BehaviorContext.h>
- #include <AzCore/Serialization/EditContext.h>
- #include <AzCore/Serialization/SerializeContext.h>
- #include <GradientSignal/Ebuses/GradientTransformRequestBus.h>
- #include <LmbrCentral/Dependency/DependencyMonitor.h>
- namespace GradientSignal
- {
- AZ::JsonSerializationResult::Result JsonImageGradientConfigSerializer::Load(
- void* outputValue, [[maybe_unused]] const AZ::Uuid& outputValueTypeId,
- const rapidjson::Value& inputValue, AZ::JsonDeserializerContext& context)
- {
- namespace JSR = AZ::JsonSerializationResult;
- auto configInstance = reinterpret_cast<ImageGradientConfig*>(outputValue);
- AZ_Assert(configInstance, "Output value for JsonImageGradientConfigSerializer can't be null.");
- JSR::ResultCode result(JSR::Tasks::ReadField);
- // The tiling field was moved from individual float values for X/Y to an AZ::Vector2,
- // so we need to handle migrating these float fields over to the vector field
- rapidjson::Value::ConstMemberIterator tilingXIter = inputValue.FindMember("TilingX");
- if (tilingXIter != inputValue.MemberEnd())
- {
- AZ::ScopedContextPath subPath(context, "TilingX");
- float tilingX;
- result.Combine(ContinueLoading(&tilingX, azrtti_typeid<float>(), tilingXIter->value, context));
- configInstance->m_tiling.SetX(tilingX);
- }
- rapidjson::Value::ConstMemberIterator tilingYIter = inputValue.FindMember("TilingY");
- if (tilingYIter != inputValue.MemberEnd())
- {
- AZ::ScopedContextPath subPath(context, "TilingY");
- float tilingY;
- result.Combine(ContinueLoading(&tilingY, azrtti_typeid<float>(), tilingYIter->value, context));
- configInstance->m_tiling.SetY(tilingY);
- }
- // We can distinguish between version 1 and 2 by the presence of the "ImageAsset" field,
- // which is only in version 1.
- // For version 2, we don't need to do any special processing, so just let the base class
- // load the JSON if we don't find the "ImageAsset" field.
- rapidjson::Value::ConstMemberIterator itr = inputValue.FindMember("ImageAsset");
- if (itr == inputValue.MemberEnd())
- {
- return AZ::BaseJsonSerializer::Load(outputValue, outputValueTypeId, inputValue, context);
- }
- // Version 1 stored a custom GradientSignal::ImageAsset as the image asset.
- // In Version 2, we changed the image asset to use the generic AZ::RPI::StreamingImageAsset,
- // so they are both AZ::Data::Asset but reference different types.
- // Using the assetHint, which will be something like "my_test_image.gradimage",
- // we need to find the valid streaming image asset product from the same source,
- // which will be something like "my_test_image.png.streamingimage"
- AZStd::string assetHint;
- AZ::Data::AssetId fixedAssetId;
- auto it = itr->value.FindMember("assetHint");
- if (it != itr->value.MemberEnd())
- {
- AZ::ScopedContextPath subPath(context, "assetHint");
- result.Combine(ContinueLoading(&assetHint, azrtti_typeid<AZStd::string>(), it->value, context));
- if (assetHint.ends_with(".gradimage"))
- {
- // We don't know what image format the original source was, so we need to loop through
- // all the supported image extensions to check if they have a valid corresponding
- // streaming image asset
- for (auto& supportedImageExtension : ImageProcessingAtom::s_SupportedImageExtensions)
- {
- AZStd::string imageExtension(supportedImageExtension);
- // The image extensions are stored with a wildcard (e.g. *.png) so we need to strip that off first
- AZ::StringFunc::Replace(imageExtension, "*", "");
- // Form potential streaming image path (e.g. my_test_image.png.streamingimage)
- AZStd::string potentialStreamingImagePath(assetHint);
- AZ::StringFunc::Replace(potentialStreamingImagePath, ".gradimage", "");
- potentialStreamingImagePath += imageExtension + ".streamingimage";
- // Check if there is a valid streaming image asset for this path
- AZ::Data::AssetCatalogRequestBus::BroadcastResult(fixedAssetId, &AZ::Data::AssetCatalogRequestBus::Events::GetAssetIdByPath, potentialStreamingImagePath.c_str(), azrtti_typeid<AZ::Data::Asset<AZ::RPI::StreamingImageAsset>>(), false);
- if (fixedAssetId.IsValid())
- {
- break;
- }
- }
- }
- }
- // The "AdvancedMode" toggle has been removed, all settings are always active and visible now.
- // If the "AdvancedMode" setting was previously disabled, make sure to set the appropriate settings to their defaults
- rapidjson::Value::ConstMemberIterator advancedModeIter = inputValue.FindMember("AdvancedMode");
- if (advancedModeIter != inputValue.MemberEnd())
- {
- AZ::ScopedContextPath subPath(context, "AdvancedMode");
- bool advancedMode = false;
- result.Combine(ContinueLoading(&advancedMode, azrtti_typeid<bool>(), advancedModeIter->value, context));
- if (!advancedMode)
- {
- configInstance->m_channelToUse = ChannelToUse::Red;
- configInstance->m_customScaleType = CustomScaleType::None;
- configInstance->m_mipIndex = 0;
- configInstance->m_samplingType = SamplingType::Point;
- }
- }
- // Replace the old gradimage with new AssetId for streaming image asset
- if (fixedAssetId.IsValid())
- {
- configInstance->m_imageAsset = AZ::Data::AssetManager::Instance().GetAsset<AZ::RPI::StreamingImageAsset>(fixedAssetId, AZ::Data::AssetLoadBehavior::QueueLoad);
- }
- return context.Report(result,
- result.GetProcessing() != JSR::Processing::Halted ?
- "Successfully loaded ImageGradientConfig information." :
- "Failed to load ImageGradientConfig information.");
- }
- AZ_CLASS_ALLOCATOR_IMPL(JsonImageGradientConfigSerializer, AZ::SystemAllocator);
- bool DoesFormatSupportTerrarium(AZ::RHI::Format format)
- {
- // The terrarium type is only supported by 8-bit formats that have
- // at least RGB
- switch (format)
- {
- case AZ::RHI::Format::R8G8B8A8_UNORM:
- case AZ::RHI::Format::R8G8B8A8_UNORM_SRGB:
- return true;
- }
- return false;
- }
- void ImageGradientConfig::Reflect(AZ::ReflectContext* context)
- {
- if (auto jsonContext = azrtti_cast<AZ::JsonRegistrationContext*>(context))
- {
- jsonContext->Serializer<JsonImageGradientConfigSerializer>()->HandlesType<ImageGradientConfig>();
- }
- AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context);
- if (serialize)
- {
- serialize->Class<ImageGradientConfig, AZ::ComponentConfig>()
- ->Version(6)
- ->Field("StreamingImageAsset", &ImageGradientConfig::m_imageAsset)
- ->Field("SamplingType", &ImageGradientConfig::m_samplingType)
- ->Field("Tiling", &ImageGradientConfig::m_tiling)
- ->Field("ChannelToUse", &ImageGradientConfig::m_channelToUse)
- ->Field("MipIndex", &ImageGradientConfig::m_mipIndex)
- ->Field("CustomScale", &ImageGradientConfig::m_customScaleType)
- ->Field("ScaleRangeMin", &ImageGradientConfig::m_scaleRangeMin)
- ->Field("ScaleRangeMax", &ImageGradientConfig::m_scaleRangeMax)
- ;
- }
- if (auto behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
- {
- behaviorContext->Class<ImageGradientConfig>()
- ->Constructor()
- ->Attribute(AZ::Script::Attributes::Category, "Vegetation")
- ->Property("tiling", BehaviorValueProperty(&ImageGradientConfig::m_tiling))
- ;
- }
- }
- bool ImageGradientConfig::GetManualScaleVisibility() const
- {
- return (m_customScaleType == CustomScaleType::Manual);
- }
- bool ImageGradientConfig::IsImageAssetReadOnly() const
- {
- return m_numImageModificationsActive > 0;
- }
- bool ImageGradientConfig::AreImageOptionsReadOnly() const
- {
- return (m_numImageModificationsActive > 0) || !(m_imageAsset.GetId().IsValid());
- }
- AZStd::string ImageGradientConfig::GetImageAssetPropertyName() const
- {
- return m_imageAssetPropertyLabel;
- }
- void ImageGradientConfig::SetImageAssetPropertyName(const AZStd::string& imageAssetPropertyName)
- {
- m_imageAssetPropertyLabel = imageAssetPropertyName;
- }
- void ImageGradientComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services)
- {
- services.push_back(AZ_CRC_CE("GradientService"));
- }
- void ImageGradientComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& services)
- {
- services.push_back(AZ_CRC_CE("GradientService"));
- }
- void ImageGradientComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& services)
- {
- services.push_back(AZ_CRC_CE("GradientTransformService"));
- }
- void ImageGradientComponent::GetDependentServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& services)
- {
- }
- void ImageGradientComponent::Reflect(AZ::ReflectContext* context)
- {
- ImageGradientConfig::Reflect(context);
- AZ::SerializeContext* serialize = azrtti_cast<AZ::SerializeContext*>(context);
- if (serialize)
- {
- serialize->Class<ImageGradientComponent, AZ::Component>()
- ->Version(0)
- ->Field("Configuration", &ImageGradientComponent::m_configuration)
- ;
- }
- if (auto behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
- {
- behaviorContext->Constant("ImageGradientComponentTypeId", BehaviorConstant(ImageGradientComponentTypeId));
- behaviorContext->Class<ImageGradientComponent>()
- ->RequestBus("ImageGradientRequestBus")
- ;
- behaviorContext->EBus<ImageGradientRequestBus>("ImageGradientRequestBus")
- ->Attribute(AZ::Script::Attributes::Category, "Vegetation/ImageGradient")
- ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation)
- ->Attribute(AZ::Script::Attributes::Module, "vegetation")
- ->Event("GetImageAssetPath", &ImageGradientRequestBus::Events::GetImageAssetPath)
- ->Event("GetImageAssetSourcePath", &ImageGradientRequestBus::Events::GetImageAssetSourcePath)
- ->Event("SetImageAssetPath", &ImageGradientRequestBus::Events::SetImageAssetPath)
- ->Event("SetImageAssetSourcePath", &ImageGradientRequestBus::Events::SetImageAssetSourcePath)
- ->VirtualProperty("ImageAssetPath", "GetImageAssetPath", "SetImageAssetPath")
- ->Event("GetTilingX", &ImageGradientRequestBus::Events::GetTilingX)
- ->Event("SetTilingX", &ImageGradientRequestBus::Events::SetTilingX)
- ->VirtualProperty("TilingX", "GetTilingX", "SetTilingX")
- ->Event("GetTilingY", &ImageGradientRequestBus::Events::GetTilingY)
- ->Event("SetTilingY", &ImageGradientRequestBus::Events::SetTilingY)
- ->VirtualProperty("TilingY", "GetTilingY", "SetTilingY")
- ;
- }
- }
- ImageGradientComponent::ImageGradientComponent(const ImageGradientConfig& configuration)
- : m_configuration(configuration)
- {
- }
- void ImageGradientComponent::GetSubImageData()
- {
- if (!m_configuration.m_imageAsset || !m_configuration.m_imageAsset.IsReady())
- {
- return;
- }
- // If we have loaded in an old image asset with an unsupported pixel format,
- // don't try to access the image data because there will be spam of asserts,
- // so just log an error message and bail out
- AZ::RHI::Format format = m_configuration.m_imageAsset->GetImageDescriptor().m_format;
- bool isFormatSupported = AZ::RPI::IsImageDataPixelAPISupported(format);
- if (!isFormatSupported)
- {
- AZ_Error("GradientSignal", false, "Image asset (%s) has an unsupported pixel format: %s",
- m_configuration.m_imageAsset.GetHint().c_str(), AZ::RHI::ToString(format));
- return;
- }
- // Prevent loading of the image data if an invalid configuration was specified by the user
- const auto numComponents = AZ::RHI::GetFormatComponentCount(format);
- const AZ::u8 channel = aznumeric_cast<AZ::u8>(m_configuration.m_channelToUse);
- if (m_configuration.m_channelToUse == ChannelToUse::Terrarium)
- {
- if (!DoesFormatSupportTerrarium(format))
- {
- AZ_Error("GradientSignal", false, "Unable to interpret image as Terrarium because image asset (%s) has pixel format (%s), which only supports %d channels",
- m_configuration.m_imageAsset.GetHint().c_str(), AZ::RHI::ToString(format), numComponents);
- return;
- }
- }
- else if (channel >= numComponents)
- {
- AZ_Error("GradientSignal", false, "Unable to use channel %d because image asset (%s) has pixel format (%s), which only supports %d channels",
- channel, m_configuration.m_imageAsset.GetHint().c_str(), AZ::RHI::ToString(format), numComponents);
- return;
- }
- m_currentChannel = m_configuration.m_channelToUse;
- m_currentScaleType = m_configuration.m_customScaleType;
- m_currentSamplingType = m_configuration.m_samplingType;
- // Make sure the custom mip level doesn't exceed the available mip levels in this
- // image asset. If so, then just use the lowest available mip level.
- auto mipLevelCount = m_configuration.m_imageAsset->GetImageDescriptor().m_mipLevels;
- m_currentMipIndex = m_configuration.m_mipIndex;
- if (m_currentMipIndex >= mipLevelCount)
- {
- AZ_Warning("GradientSignal", false, "Mip level index (%d) out of bounds, only %d levels available. Using lowest available mip level",
- m_currentMipIndex, mipLevelCount);
- m_currentMipIndex = aznumeric_cast<AZ::u32>(mipLevelCount) - 1;
- }
- // Update our cached image data
- UpdateCachedImageBufferData(
- m_configuration.m_imageAsset->GetImageDescriptorForMipLevel(m_currentMipIndex),
- m_configuration.m_imageAsset->GetSubImageData(m_currentMipIndex, 0));
- // Calculate the multiplier and offset based on our scale type
- // Make sure we do this last, because the calculation might
- // depend on the image data (e.g. auto scale finds the min/max value
- // from the image data, which might be different based on the mip level)
- switch (m_currentScaleType)
- {
- case CustomScaleType::Auto:
- SetupAutoScaleMultiplierAndOffset();
- break;
- case CustomScaleType::Manual:
- SetupManualScaleMultiplierAndOffset();
- break;
- case CustomScaleType::None:
- default:
- SetupDefaultMultiplierAndOffset();
- break;
- }
- }
- float ImageGradientComponent::GetValueFromImageData(SamplingType samplingType, const AZ::Vector3& uvw, float defaultValue) const
- {
- if (!m_imageData.empty())
- {
- const auto width = m_imageDescriptor.m_size.m_width;
- const auto height = m_imageDescriptor.m_size.m_height;
- if (width > 0 && height > 0)
- {
- // When "rasterizing" from uvs, a range of 0-1 has slightly different meanings depending on the sampler state.
- // For repeating states (Unbounded/None, Repeat), a uv value of 1 should wrap around back to our 0th pixel.
- // For clamping states (Clamp to Zero, Clamp to Edge), a uv value of 1 should point to the last pixel.
- // We assume here that the code handling sampler states has handled this for us in the clamping cases
- // by reducing our uv by a small delta value such that anything that wants the last pixel has a value
- // just slightly less than 1.
- // Keeping that in mind, we scale our uv from 0-1 to 0-image size inclusive. So a 4-pixel image will scale
- // uv values of 0-1 to 0-4, not 0-3 as you might expect. This is because we want the following range mappings:
- // [0 - 1/4) = pixel 0
- // [1/4 - 1/2) = pixel 1
- // [1/2 - 3/4) = pixel 2
- // [3/4 - 1) = pixel 3
- // [1 - 1 1/4) = pixel 0
- // ...
- // Also, based on our tiling settings, we extend the size of our image virtually by a factor of tilingX and tilingY.
- // A 16x16 pixel image and tilingX = tilingY = 1 maps the uv range of 0-1 to 0-16 pixels.
- // A 16x16 pixel image and tilingX = tilingY = 1.5 maps the uv range of 0-1 to 0-24 pixels.
- const AZ::Vector2 tiledDimensions(width * GetTilingX(), height * GetTilingY());
- // Convert from uv space back to pixel space
- AZ::Vector2 pixelLookup = (AZ::Vector2(uvw) * tiledDimensions);
- // UVs outside the 0-1 range are treated as infinitely tiling, so that we behave the same as the
- // other gradient generators. As mentioned above, if clamping is desired, we expect it to be applied
- // outside of this function.
- float pixelX = pixelLookup.GetX();
- float pixelY = pixelLookup.GetY();
- auto x = aznumeric_cast<AZ::u32>(pixelX) % width;
- auto y = aznumeric_cast<AZ::u32>(pixelY) % height;
- // Retrieve our pixel value based on our sampling type
- const float value = GetValueForSamplingType(samplingType, x, y, pixelX, pixelY);
- // Scale (inverse lerp) the value into a 0 - 1 range. We also clamp it because manual scale values could cause
- // the result to fall outside of the expected output range.
- return AZStd::clamp((value - m_offset) * m_multiplier, 0.0f, 1.0f);
- }
- }
- return defaultValue;
- }
- float ImageGradientComponent::InvertYAndGetPixelValue(AZ::u32 x, AZ::u32 invertedY) const
- {
- // This is a convenience method that flips the y before calling GetPixelValue() because
- // image heights are stored in the reverse direction of our world axes.
- const auto height = m_imageDescriptor.m_size.m_height;
- AZ::u32 y = (height - 1) - invertedY;
- return GetPixelValue(x, y);
- }
- float ImageGradientComponent::GetPixelValue(AZ::u32 x, AZ::u32 y) const
- {
- // For terrarium, there is a separate algorithm for retrieving the value
- float value = (m_currentChannel == ChannelToUse::Terrarium)
- ? GetTerrariumPixelValue(x, y)
- : AZ::RPI::GetImageDataPixelValue<float>(
- m_imageData, m_imageDescriptor, x, y, aznumeric_cast<AZ::u8>(m_currentChannel));
- return value;
- }
- float ImageGradientComponent::GetTerrariumPixelValue(AZ::u32 x, AZ::u32 y) const
- {
- float r = AZ::RPI::GetImageDataPixelValue<float>(m_imageData, m_imageDescriptor, x, y, aznumeric_cast<AZ::u8>(ChannelToUse::Red));
- float g = AZ::RPI::GetImageDataPixelValue<float>(m_imageData, m_imageDescriptor, x, y, aznumeric_cast<AZ::u8>(ChannelToUse::Green));
- float b = AZ::RPI::GetImageDataPixelValue<float>(m_imageData, m_imageDescriptor, x, y, aznumeric_cast<AZ::u8>(ChannelToUse::Blue));
- /*
- "Terrarium" is an image-based terrain file format as defined here: https://www.mapzen.com/blog/terrain-tile-service/
- According to the website: "Terrarium format PNG tiles contain raw elevation data in meters, in Mercator projection (EPSG:3857).
- All values are positive with a 32,768 offset, split into the red, green, and blue channels, with 16 bits of integer and 8 bits of fraction. To decode: (red * 256 + green + blue / 256) - 32768"
- This gives a range -32768 to 32768 meters at a constant 1/256 meter resolution. For reference, the lowest point on Earth (Mariana Trench) is at -10911 m, and the highest point (Mt Everest) is at 8848 m.
- The equation of (red * 256 + green + blue / 256) - 32768 is based on red/green/blue being u8 values, but we are getting float values back
- in the range of 0.0f - 1.0f, so the multipliers below have been modified slightly to account for that scaling
- */
- constexpr float redMultiplier = (255.0f * 256.0f) / 65536.0f;
- constexpr float greenMultiplier = 255.0f / 65536.0f;
- constexpr float blueMultiplier = (255.0f / 256.0f) / 65536.0f;
- return (r * redMultiplier) + (g * greenMultiplier) + (b * blueMultiplier);
- }
- void ImageGradientComponent::SetupMultiplierAndOffset(float min, float max)
- {
- // Pre-calculate values for scaling our input range to our output range of 0 - 1. Scaling just uses the standard inverse lerp
- // formula of "output = (input - min) / (max - min)", or "output = (input - offset) * multiplier" where
- // multiplier is 1 / (max - min) and offset is min. Precalculating this way lets us gracefully handle the case where min and
- // max are equal, since we don't want to divide by infinity, without needing to check for that case on every pixel.
- // If our range is equivalent, set our multiplier and offset so that
- // any input value > min goes to 1 and any input value <= min goes to 0.
- m_multiplier = (min == max) ? AZStd::numeric_limits<float>::max() : (1.0f / (max - min));
- m_offset = min;
- }
- void ImageGradientComponent::SetupDefaultMultiplierAndOffset()
- {
- // By default, don't perform any scaling - assume the input range is from 0 - 1, same as the desired output.
- m_minValue = 0.0f;
- m_maxValue = 1.0f;
- SetupMultiplierAndOffset(m_minValue, m_maxValue);
- }
- void ImageGradientComponent::SetupAutoScaleMultiplierAndOffset()
- {
- auto width = m_imageDescriptor.m_size.m_width;
- auto height = m_imageDescriptor.m_size.m_height;
- float minValue = AZStd::numeric_limits<float>::max();
- float maxValue = AZStd::numeric_limits<float>::lowest();
- // By looping through and calling GetPixelValue(), this will correctly get the min/max values from
- // either our image data or our modification buffer.
- for (uint32_t y = 0; y < height; y++)
- {
- for (uint32_t x = 0; x < width; x++)
- {
- float value = GetPixelValue(x, y);
- minValue = AZStd::min(value, minValue);
- maxValue = AZStd::max(value, maxValue);
- }
- }
- // Set our multiplier and offset based on the min / max values we found.
- m_minValue = minValue;
- m_maxValue = maxValue;
- SetupMultiplierAndOffset(m_minValue, m_maxValue);
- }
- void ImageGradientComponent::SetupManualScaleMultiplierAndOffset()
- {
- m_configuration.m_scaleRangeMin = AZStd::clamp(m_configuration.m_scaleRangeMin, 0.0f, 1.0f);
- m_configuration.m_scaleRangeMax = AZStd::clamp(m_configuration.m_scaleRangeMax, 0.0f, 1.0f);
- // Set our multiplier and offset based on the manual scale range. Note that the manual scale range might be less than the
- // input range and possibly even inverted.
- m_minValue = m_configuration.m_scaleRangeMin;
- m_maxValue = m_configuration.m_scaleRangeMax;
- SetupMultiplierAndOffset(m_minValue, m_maxValue);
- }
- float ImageGradientComponent::GetClampedValue(int32_t x, int32_t y) const
- {
- const auto width = m_imageDescriptor.m_size.m_width;
- const auto height = m_imageDescriptor.m_size.m_height;
- switch (m_gradientTransform.GetWrappingType())
- {
- case WrappingType::ClampToZero:
- if (x < 0 || x > m_maxX || y < 0 || y > m_maxY)
- {
- return 0.0f;
- }
- break;
- case WrappingType::ClampToEdge:
- x = AZ::GetClamp(x, 0, m_maxX);
- y = AZ::GetClamp(y, 0, m_maxY);
- break;
- case WrappingType::Mirror:
- if (x < 0)
- {
- x = -x;
- }
- if (y < 0)
- {
- y = -y;
- }
- if (x > m_maxX)
- {
- x = m_maxX - (x % width);
- }
- if (y > m_maxY)
- {
- y = m_maxY - (y % height);
- }
- break;
- case WrappingType::None:
- case WrappingType::Repeat:
- default:
- x = x % width;
- y = y % height;
- break;
- }
- return InvertYAndGetPixelValue(x, y);
- }
- void ImageGradientComponent::Get4x4Neighborhood(uint32_t x, uint32_t y, AZStd::array<AZStd::array<float, 4>, 4>& values) const
- {
- for (int32_t yIndex = 0; yIndex < 4; ++yIndex)
- {
- for (int32_t xIndex = 0; xIndex < 4; ++xIndex)
- {
- values[xIndex][yIndex] = GetClampedValue(x + xIndex - 1, y + yIndex - 1);
- }
- }
- }
- float ImageGradientComponent::GetValueForSamplingType(SamplingType samplingType, AZ::u32 x0, AZ::u32 y0, float pixelX, float pixelY) const
- {
- switch (samplingType)
- {
- case SamplingType::Point:
- default:
- // Retrieve the pixel value for the single point
- return InvertYAndGetPixelValue(x0, y0);
- case SamplingType::Bilinear:
- {
- // Bilinear interpolation
- //
- // |
- // |
- // | (x0,y1) * * (x1,y1)
- // |
- // | o (x,y)
- // |
- // | (x0,y0) * * (x1,y0)
- // |___________________________________
- //
- // The bilinear filtering samples from a grid around a desired pixel (x,y)
- // x0,y0 contains one corner of our grid square, x1,y1 contains the opposite corner, and deltaX/Y is the fractional
- // amount the position exists between those corners.
- // Ex: (3.3, 4.4) would have a x0,y0 of (3, 4), a x1,y1 of (4, 5), and a deltaX/Y of (0.3, 0.4).
- const float valueX0Y0 = GetClampedValue(x0, y0);
- const float valueX1Y0 = GetClampedValue(x0 + 1, y0);
- const float valueX0Y1 = GetClampedValue(x0, y0 + 1);
- const float valueX1Y1 = GetClampedValue(x0 + 1, y0 + 1);
- float deltaX = pixelX - floor(pixelX);
- float deltaY = pixelY - floor(pixelY);
- const float valueXY0 = AZ::Lerp(valueX0Y0, valueX1Y0, deltaX);
- const float valueXY1 = AZ::Lerp(valueX0Y1, valueX1Y1, deltaX);
- return AZ::Lerp(valueXY0, valueXY1, deltaY);
- }
- case SamplingType::Bicubic:
- {
- // Catmull-Rom style bicubic filtering. This uses the neighborhood of 16 samples to calculate a smooth curve for values
- // in between discrete sample locations. See https://en.wikipedia.org/wiki/Bicubic_interpolation
- // Simplified interpolation function
- auto cubicInterpolate = [](float p0, float p1, float p2, float p3, float delta) -> float
- {
- return p1 + 0.5f * delta * (p2 - p0 + delta * (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3 + delta * (3.0f * (p1 - p2) + p3 - p0)));
- };
- AZStd::array<AZStd::array<float, 4>, 4> values;
- Get4x4Neighborhood(x0, y0, values);
- float deltaX = pixelX - floor(pixelX);
- float deltaY = pixelY - floor(pixelY);
- const float valueXY0 = cubicInterpolate(values[0][0], values[1][0], values[2][0], values[3][0], deltaX);
- const float valueXY1 = cubicInterpolate(values[0][1], values[1][1], values[2][1], values[3][1], deltaX);
- const float valueXY2 = cubicInterpolate(values[0][2], values[1][2], values[2][2], values[3][2], deltaX);
- const float valueXY3 = cubicInterpolate(values[0][3], values[1][3], values[2][3], values[3][3], deltaX);
- return cubicInterpolate(valueXY0, valueXY1, valueXY2, valueXY3, deltaY);
- }
- }
- }
- void ImageGradientComponent::Activate()
- {
- // This will immediately call OnGradientTransformChanged and initialize m_gradientTransform.
- GradientTransformNotificationBus::Handler::BusConnect(GetEntityId());
- ImageGradientRequestBus::Handler::BusConnect(GetEntityId());
- AzFramework::PaintBrushNotificationBus::Handler::BusConnect({ GetEntityId(), GetId() });
- ImageGradientModificationBus::Handler::BusConnect(GetEntityId());
- // Invoke the QueueLoad before connecting to the AssetBus, so that
- // if the asset is already ready, then OnAssetReady will be triggered immediately
- UpdateCachedImageBufferData({}, {});
- m_configuration.m_imageAsset.QueueLoad(AZ::Data::AssetLoadParameters(nullptr, AZ::Data::AssetDependencyLoadRules::LoadAll));
- AZ::Data::AssetBus::Handler::BusConnect(m_configuration.m_imageAsset.GetId());
- // Connect to GradientRequestBus last so that everything is initialized before listening for gradient queries.
- GradientRequestBus::Handler::BusConnect(GetEntityId());
- }
- void ImageGradientComponent::Deactivate()
- {
- // Disconnect from GradientRequestBus first to ensure no queries are in process when deactivating.
- GradientRequestBus::Handler::BusDisconnect();
- AZ::Data::AssetBus::Handler::BusDisconnect();
- ImageGradientModificationBus::Handler::BusDisconnect();
- AzFramework::PaintBrushNotificationBus::Handler::BusDisconnect();
- ImageGradientRequestBus::Handler::BusDisconnect();
- GradientTransformNotificationBus::Handler::BusDisconnect();
- // Make sure we don't keep any cached references to the image asset data or the image modification buffer.
- UpdateCachedImageBufferData({}, {});
- m_configuration.m_imageAsset.Release();
- }
- bool ImageGradientComponent::ReadInConfig(const AZ::ComponentConfig* baseConfig)
- {
- if (auto config = azrtti_cast<const ImageGradientConfig*>(baseConfig))
- {
- m_configuration = *config;
- return true;
- }
- return false;
- }
- bool ImageGradientComponent::WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const
- {
- if (auto config = azrtti_cast<ImageGradientConfig*>(outBaseConfig))
- {
- *config = m_configuration;
- return true;
- }
- return false;
- }
- void ImageGradientComponent::UpdateCachedImageBufferData(
- const AZ::RHI::ImageDescriptor& imageDescriptor, AZStd::span<const uint8_t> imageData)
- {
- bool shouldRefreshModificationBuffer = false;
- // If we're changing our image data from our modification buffer to something else while it's active,
- // let's refresh the modification buffer with the new data.
- if (ModificationBufferIsActive() && (imageData.data() != m_imageData.data()))
- {
- shouldRefreshModificationBuffer = true;
- }
- m_imageDescriptor = imageDescriptor;
- m_imageData = imageData;
- m_maxX = imageDescriptor.m_size.m_width - 1;
- m_maxY = imageDescriptor.m_size.m_height - 1;
- if (shouldRefreshModificationBuffer)
- {
- m_modifiedImageData.resize(0);
- if (!m_imageData.empty())
- {
- CreateImageModificationBuffer();
- }
- }
- }
- void ImageGradientComponent::OnAssetReady(AZ::Data::Asset<AZ::Data::AssetData> asset)
- {
- {
- AZStd::unique_lock lock(m_queryMutex);
- m_configuration.m_imageAsset = asset;
- GetSubImageData();
- }
- LmbrCentral::DependencyNotificationBus::Event(GetEntityId(), &LmbrCentral::DependencyNotificationBus::Events::OnCompositionChanged);
- }
- void ImageGradientComponent::OnAssetReloaded(AZ::Data::Asset<AZ::Data::AssetData> asset)
- {
- OnAssetReady(asset);
- }
- void ImageGradientComponent::OnGradientTransformChanged(const GradientTransform& newTransform)
- {
- AZStd::unique_lock lock(m_queryMutex);
- m_gradientTransform = newTransform;
- }
- void ImageGradientComponent::OnPaintModeBegin()
- {
- StartImageModification();
- }
- void ImageGradientComponent::OnPaintModeEnd()
- {
- EndImageModification();
- }
- AZ::Color ImageGradientComponent::OnGetColor(const AZ::Vector3& brushCenter) const
- {
- // Get the gradient value at the given point.
- // We use "GetPixelValuesByPosition" instead of "GetGradientValue" because we want to select unscaled, unsmoothed values.
- float gradientValue = 0.0f;
- GetPixelValuesByPosition(AZStd::span<const AZ::Vector3>(&brushCenter, 1), AZStd::span<float>(&gradientValue, 1));
- return AZ::Color(gradientValue, gradientValue, gradientValue, 1.0f);
- }
- void ImageGradientComponent::StartImageModification()
- {
- if (!m_imageModifier)
- {
- AZ_Assert(m_configuration.m_numImageModificationsActive == 0,
- "The imageModifier should exist since image modifications are already currently active.");
- m_imageModifier = AZStd::make_unique<ImageGradientModifier>(AZ::EntityComponentIdPair(GetEntityId(), GetId()));
- }
- if (m_modifiedImageData.empty())
- {
- CreateImageModificationBuffer();
- }
- m_configuration.m_numImageModificationsActive++;
- }
- void ImageGradientComponent::EndImageModification()
- {
- AZ_Assert(m_configuration.m_numImageModificationsActive > 0, "Mismatched calls to StartImageModification / EndImageModification");
- m_configuration.m_numImageModificationsActive--;
- if (m_configuration.m_numImageModificationsActive == 0)
- {
- m_imageModifier = {};
- }
- }
- AZStd::vector<float>* ImageGradientComponent::GetImageModificationBuffer()
- {
- // This will get replaced with safe/robust methods of modifying the image as paintbrush functionality
- // continues to get added to the Image Gradient component.
- return &m_modifiedImageData;
- }
- bool ImageGradientComponent::ImageIsModified() const
- {
- return !m_modifiedImageData.empty() && m_imageIsModified;
- }
- void ImageGradientComponent::CreateImageModificationBuffer()
- {
- if (m_imageData.empty())
- {
- AZ_Error("ImageGradientComponent", false,
- "Image data is empty. Make sure the image asset is fully loaded before attempting to modify it.");
- return;
- }
- const auto width = m_imageDescriptor.m_size.m_width;
- const auto height = m_imageDescriptor.m_size.m_height;
- // Track that the image hasn't been modified yet, even though we've created a modification buffer.
- m_imageIsModified = false;
- if (m_modifiedImageData.empty())
- {
- // Create a memory buffer for holding all of our modified image information.
- // We'll always use a buffer of floats to ensure that we're modifying at the highest precision possible.
- m_modifiedImageData.reserve(width * height);
- // Fill the buffer with all of our existing pixel values.
- for (uint32_t y = 0; y < height; y++)
- {
- for (uint32_t x = 0; x < width; x++)
- {
- float pixel = AZ::RPI::GetImageDataPixelValue<float>(
- m_imageData, m_imageDescriptor, x, y, aznumeric_cast<AZ::u8>(m_currentChannel));
- m_modifiedImageData.emplace_back(pixel);
- }
- }
- // Create an image descriptor describing our new buffer (correct width, height, and single-channel 32-bit float format)
- auto imageDescriptor =
- AZ::RHI::ImageDescriptor::Create2D(AZ::RHI::ImageBindFlags::None, width, height, AZ::RHI::Format::R32_FLOAT);
- // Set our imageData pointer to point to our modified data buffer.
- auto imageData = AZStd::span<const uint8_t>(
- reinterpret_cast<uint8_t*>(m_modifiedImageData.data()), m_modifiedImageData.size() * sizeof(float));
- UpdateCachedImageBufferData(imageDescriptor, imageData);
- }
- else
- {
- // If this triggers, we've somehow gotten our image modification buffer out of sync with the image descriptor information.
- AZ_Assert(m_modifiedImageData.size() == (width * height), "Image modification buffer exists but is the wrong size.");
- }
- }
- void ImageGradientComponent::ClearImageModificationBuffer()
- {
- AZ_Assert(m_configuration.m_numImageModificationsActive == 0, "Clearing modified image data while in modification mode!")
- m_modifiedImageData.resize(0);
- m_imageIsModified = false;
- }
- bool ImageGradientComponent::ModificationBufferIsActive() const
- {
- // The modification buffer is considered active if the modification buffer has data in it and
- // our cached imageData pointer is pointing into the modification buffer instead of into an image asset.
- return (m_modifiedImageData.data() != nullptr) && (!m_modifiedImageData.empty()) &&
- (reinterpret_cast<const void*>(m_imageData.data()) == reinterpret_cast<const void*>(m_modifiedImageData.data()));
- }
- float ImageGradientComponent::GetValue(const GradientSampleParams& sampleParams) const
- {
- AZ::Vector3 position(sampleParams.m_position);
- float value = 0.0f;
- GetValuesInternal(m_currentSamplingType, AZStd::span<AZ::Vector3>(&position, 1), AZStd::span<float>(&value, 1));
- return value;
- }
- void ImageGradientComponent::GetValues(AZStd::span<const AZ::Vector3> positions, AZStd::span<float> outValues) const
- {
- GetValuesInternal(m_currentSamplingType, positions, outValues);
- }
- void ImageGradientComponent::GetValuesInternal(
- SamplingType samplingType, AZStd::span<const AZ::Vector3> positions, AZStd::span<float> outValues) const
- {
- if (positions.size() != outValues.size())
- {
- AZ_Assert(false, "input and output lists are different sizes (%zu vs %zu).", positions.size(), outValues.size());
- return;
- }
- AZStd::shared_lock lock(m_queryMutex);
- // Just clear the output values and return if our cached image data hasn't been retrieved yet
- if (m_imageData.empty())
- {
- AZStd::fill(outValues.begin(), outValues.end(), 0.0f);
- return;
- }
- AZ::Vector3 uvw;
- bool wasPointRejected = false;
- for (size_t index = 0; index < positions.size(); index++)
- {
- m_gradientTransform.TransformPositionToUVWNormalized(positions[index], uvw, wasPointRejected);
- if (!wasPointRejected)
- {
- outValues[index] = GetValueFromImageData(samplingType, uvw, 0.0f);
- }
- else
- {
- outValues[index] = 0.0f;
- }
- }
- }
- AZStd::string ImageGradientComponent::GetImageAssetPath() const
- {
- AZStd::string assetPathString;
- AZ::Data::AssetCatalogRequestBus::BroadcastResult(assetPathString, &AZ::Data::AssetCatalogRequests::GetAssetPathById, m_configuration.m_imageAsset.GetId());
- return assetPathString;
- }
- AZStd::string ImageGradientComponent::GetImageAssetSourcePath() const
- {
- // The m_imageAsset path is to the product, so it will have an additional extension:
- // e.g. image.png.streamingimage
- // So to provide just the source asset path we need to remove the product extension
- AZStd::string imageAssetPath = GetImageAssetPath();
- AZ::IO::Path imageSourceAssetPath = AZ::IO::Path(imageAssetPath).ReplaceExtension("");
- return imageSourceAssetPath.c_str();
- }
- void ImageGradientComponent::SetImageAssetPath(const AZStd::string& assetPath)
- {
- AZ::Data::AssetId assetId;
- if (!assetPath.empty())
- {
- AZ::Data::AssetCatalogRequestBus::BroadcastResult(
- assetId, &AZ::Data::AssetCatalogRequests::GetAssetIdByPath, assetPath.c_str(), AZ::Data::s_invalidAssetType, false);
- if (!assetId.IsValid())
- {
- // This case can occur either if the asset path is completely wrong, or if it's correct but the asset is still in
- // the process of being created and being processed. Even though the second possibility is valid,
- AZ_Warning(
- "GradientSignal", false, "Can't find an Asset ID for %s, SetImageAssetPath() will be ignored.", assetPath.c_str());
- return;
- }
- }
- // If we were given a valid asset, then make sure it is the right type
- if (assetId.IsValid())
- {
- AZ::Data::AssetInfo assetInfo;
- AZ::Data::AssetCatalogRequestBus::BroadcastResult(assetInfo, &AZ::Data::AssetCatalogRequests::GetAssetInfoById, assetId);
- if (assetInfo.m_assetType != azrtti_typeid<AZ::RPI::StreamingImageAsset>())
- {
- AZ_Warning("GradientSignal", false, "Asset type for %s is not AZ::RPI::StreamingImageAsset, will be ignored", assetPath.c_str());
- return;
- }
- }
- AZ::Data::Asset<AZ::RPI::StreamingImageAsset> imageAsset;
- if (assetId.IsValid())
- {
- imageAsset = AZ::Data::AssetManager::Instance().FindOrCreateAsset(
- assetId, azrtti_typeid<AZ::RPI::StreamingImageAsset>(), m_configuration.m_imageAsset.GetAutoLoadBehavior());
- }
- SetImageAsset(imageAsset);
- }
- void ImageGradientComponent::SetImageAssetSourcePath(const AZStd::string& assetPath)
- {
- // SetImageAssetPath expects a product asset path, so we need to append the product
- // extension to the source asset path we are given
- AZStd::string productAssetPath(assetPath);
- productAssetPath += ".streamingimage";
- SetImageAssetPath(productAssetPath);
- }
- AZ::Data::Asset<AZ::RPI::StreamingImageAsset> ImageGradientComponent::GetImageAsset() const
- {
- return m_configuration.m_imageAsset;
- }
- void ImageGradientComponent::SetImageAsset(const AZ::Data::Asset<AZ::RPI::StreamingImageAsset>& asset)
- {
- // If we're setting the component to the same asset we're already using, then early-out.
- if (asset.GetId() == m_configuration.m_imageAsset.GetId())
- {
- return;
- }
- // Stop listening for the current image asset.
- AZ::Data::AssetBus::Handler::BusDisconnect(m_configuration.m_imageAsset.GetId());
- {
- // Only hold the lock during the actual data changes, to ensure that we aren't mid-query when changing it, but also to
- // minimize the total lock duration. Also, because we've disconnected from the imageAsset Asset bus prior to locking this,
- // we won't get any OnAsset* notifications while we're changing out the asset.
- AZStd::unique_lock lock(m_queryMutex);
- // Clear our cached image data unless we're currently using a modification buffer.
- // If we're using a modification buffer, we want to keep it active until the new image has finished loading in.
- if (!asset.IsReady() && !ModificationBufferIsActive())
- {
- UpdateCachedImageBufferData({}, {});
- }
- m_configuration.m_imageAsset = asset;
- }
- if (m_configuration.m_imageAsset.GetId().IsValid())
- {
- // If we have a valid Asset ID, check to see if it also appears in the AssetCatalog. This might be an Asset ID for an asset
- // that doesn't exist yet if it was just created from the Editor component.
- AZ::Data::AssetInfo assetInfo;
- AZ::Data::AssetCatalogRequestBus::BroadcastResult(
- assetInfo, &AZ::Data::AssetCatalogRequests::GetAssetInfoById, m_configuration.m_imageAsset.GetId());
- // Only queue the load if it appears in the Asset Catalog. If it doesn't, we'll get notified when it shows up.
- if (assetInfo.m_assetId.IsValid())
- {
- m_configuration.m_imageAsset.QueueLoad(AZ::Data::AssetLoadParameters(nullptr, AZ::Data::AssetDependencyLoadRules::LoadAll));
- }
- // Start listening for all events for this asset.
- AZ::Data::AssetBus::Handler::BusConnect(m_configuration.m_imageAsset.GetId());
- }
- LmbrCentral::DependencyNotificationBus::Event(GetEntityId(), &LmbrCentral::DependencyNotificationBus::Events::OnCompositionChanged);
- }
- uint32_t ImageGradientComponent::GetImageHeight() const
- {
- return m_imageDescriptor.m_size.m_height;
- }
- uint32_t ImageGradientComponent::GetImageWidth() const
- {
- return m_imageDescriptor.m_size.m_width;
- }
- AZ::Vector2 ImageGradientComponent::GetImagePixelsPerMeter() const
- {
- // Get the number of pixels in our image that maps to each meter based on the tiling and gradient transform settings.
- const auto width = m_imageDescriptor.m_size.m_width;
- const auto height = m_imageDescriptor.m_size.m_height;
- if (width > 0 && height > 0)
- {
- // The number of pixels per meter depends on a combination of the tiling, the scale, and the frequency zoom.
- // All of these numbers together determine how often the image repeats within the shape bounds.
- const AZ::Vector3 transformScale = m_gradientTransform.GetScale();
- const float transformZoom = m_gradientTransform.GetFrequencyZoom();
- // Get the local bounds for the gradient. This determines the total number of meters in each direction that the image
- // repeats are mapped onto.
- const AZ::Aabb localBounds = m_gradientTransform.GetBounds();
- const AZ::Vector2 boundsMeters(localBounds.GetExtents());
- const AZ::Vector2 imagePixelsInBounds(width * GetTilingX(), height * GetTilingY());
- return (imagePixelsInBounds * transformZoom) / (boundsMeters * AZ::Vector2(transformScale));
- }
- return AZ::Vector2::CreateZero();
- }
- float ImageGradientComponent::GetTilingX() const
- {
- return m_configuration.m_tiling.GetX();
- }
- void ImageGradientComponent::SetTilingX(float tilingX)
- {
- // Only hold the lock while we're changing the data. Don't hold onto it during the OnCompositionChanged call, because that can
- // execute an arbitrary amount of logic, including calls back to this component.
- {
- AZStd::unique_lock lock(m_queryMutex);
- m_configuration.m_tiling.SetX(tilingX);
- }
- LmbrCentral::DependencyNotificationBus::Event(GetEntityId(), &LmbrCentral::DependencyNotificationBus::Events::OnCompositionChanged);
- }
- float ImageGradientComponent::GetTilingY() const
- {
- return m_configuration.m_tiling.GetY();
- }
- void ImageGradientComponent::SetTilingY(float tilingY)
- {
- // Only hold the lock while we're changing the data. Don't hold onto it during the OnCompositionChanged call, because that can
- // execute an arbitrary amount of logic, including calls back to this component.
- {
- AZStd::unique_lock lock(m_queryMutex);
- m_configuration.m_tiling.SetY(tilingY);
- }
- LmbrCentral::DependencyNotificationBus::Event(GetEntityId(), &LmbrCentral::DependencyNotificationBus::Events::OnCompositionChanged);
- }
- PixelIndex ImageGradientComponent::GetPixelIndexForPositionInternal(const AZ::Vector3& position) const
- {
- const auto width = m_imageDescriptor.m_size.m_width;
- const auto height = m_imageDescriptor.m_size.m_height;
- const AZ::Vector3 tiledDimensions((width * GetTilingX()), (height * GetTilingY()), 0.0f);
- // Use the Gradient Transform to convert from world space to image space.
- AZ::Vector3 uvw = position;
- bool wasPointRejected = true;
- m_gradientTransform.TransformPositionToUVWNormalized(position, uvw, wasPointRejected);
- if ((width > 0) && (height > 0) && (!wasPointRejected))
- {
- // Since the Image Gradient also has a tiling factor, scale the returned image space value
- // by the tiling factor to get to the specific pixel requested.
- AZ::Vector3 pixelLookup = (uvw * tiledDimensions);
- // UVs outside the 0-1 range are treated as infinitely tiling, we mod the values to bring them back into image bounds.
- float pixelX = pixelLookup.GetX();
- float pixelY = pixelLookup.GetY();
- auto x = aznumeric_cast<AZ::u32>(pixelX) % width;
- auto y = aznumeric_cast<AZ::u32>(pixelY) % height;
- // Flip the y because images are stored in reverse of our world axes
- y = (height - 1) - y;
- return PixelIndex(aznumeric_cast<int16_t>(x), aznumeric_cast<int16_t>(y));
- }
- else
- {
- return PixelIndex(aznumeric_cast<int16_t>(-1), aznumeric_cast<int16_t>(-1));
- }
- }
- bool ImageGradientComponent::PixelIndexIsValid(const PixelIndex& pixelIndex) const
- {
- const auto width = m_imageDescriptor.m_size.m_width;
- const auto height = m_imageDescriptor.m_size.m_height;
- const auto& [x, y] = pixelIndex;
- return ((x >= 0) && (x < aznumeric_cast<int16_t>(width)) && (y >= 0) && (y < aznumeric_cast<int16_t>(height)));
- }
- void ImageGradientComponent::GetPixelValuesByPosition(AZStd::span<const AZ::Vector3> positions, AZStd::span<float> outValues) const
- {
- AZStd::shared_lock lock(m_queryMutex);
- for (size_t index = 0; index < positions.size(); index++)
- {
- // We use the Pixel* APIs because we want to end up with raw unscaled, unsmoothed pixel values instead of final scaled
- // and smoothed gradient values.
- auto pixelIndex = GetPixelIndexForPositionInternal(positions[index]);
- if (PixelIndexIsValid(pixelIndex))
- {
- const auto& [x, y] = pixelIndex;
- outValues[index] = GetPixelValue(x, y);
- // This intentionally does not scale the pixel values with the multiplier and offset. We're trying to
- // get pixel values, not final gradient values.
- }
- }
- }
- void ImageGradientComponent::GetPixelIndicesForPositions(
- AZStd::span<const AZ::Vector3> positions, AZStd::span<PixelIndex> outIndices) const
- {
- AZStd::shared_lock lock(m_queryMutex);
- for (size_t index = 0; index < positions.size(); index++)
- {
- outIndices[index] = GetPixelIndexForPositionInternal(positions[index]);
- }
- }
- void ImageGradientComponent::GetPixelValuesByPixelIndex(AZStd::span<const PixelIndex> positions, AZStd::span<float> outValues) const
- {
- AZStd::shared_lock lock(m_queryMutex);
- for (size_t index = 0; index < positions.size(); index++)
- {
- if (PixelIndexIsValid(positions[index]))
- {
- const auto& [x, y] = positions[index];
- // For terrarium, there is a separate algorithm for retrieving the value.
- outValues[index] = GetPixelValue(x, y);
- // This intentionally does not scale the pixel values with the multiplier and offset. We're trying to
- // get pixel values, not final gradient values.
- }
- }
- }
- void ImageGradientComponent::SetPixelValuesByPosition(AZStd::span<const AZ::Vector3> positions, AZStd::span<const float> values)
- {
- AZStd::vector<PixelIndex> pixelIndices(positions.size());
- GetPixelIndicesForPositions(positions, pixelIndices);
- SetPixelValuesByPixelIndex(pixelIndices, values);
- }
- void ImageGradientComponent::SetPixelValuesByPixelIndex(AZStd::span<const PixelIndex> positions, AZStd::span<const float> values)
- {
- bool refreshEntireImage = false;
- // We perform all the modfications with the scoped queryMutex lock. This may cause us to send an OnCompositionChanged() message,
- // which we'll need to do outside of the lock to avoid any potential deadlocks.
- {
- AZStd::unique_lock lock(m_queryMutex);
- if (m_modifiedImageData.empty())
- {
- AZ_Error(
- "ImageGradientComponent", false, "Image modification mode needs to be started before the image values can be set.");
- return;
- }
- const auto width = m_imageDescriptor.m_size.m_width;
- const auto height = m_imageDescriptor.m_size.m_height;
- // No pixels, so nothing to modify.
- if ((width == 0) || (height == 0))
- {
- return;
- }
- // If we're set to auto-scaling, we need to do a bit more tracking while modifying our data to see if our auto-scaling
- // values have changed. If so, we'll need to refresh the entire image.
- if (m_currentScaleType == CustomScaleType::Auto)
- {
- const float preModificationMinValue = m_minValue;
- const float preModificationMaxValue = m_maxValue;
- // This tracks whether or not we need to loop through *all* the pixels to recalculate the min/max values.
- bool recalculateMinMax = false;
- for (size_t index = 0; index < positions.size(); index++)
- {
- if (PixelIndexIsValid(positions[index]))
- {
- const auto& [x, y] = positions[index];
- auto& pixelToModify = m_modifiedImageData[(y * width) + x];
- // If the value we're modifying was previously either our old min or old max value,
- // and we're setting it to a value that's inside the min/max range, then we're going to need
- // to fully recalculate our min/max values and our scaling multiplier and offset, since
- // the range might have shrunk. If they've changed, then we'll need to refresh the entire image.
- if ((pixelToModify == preModificationMinValue) && (values[index] > preModificationMinValue))
- {
- recalculateMinMax = true;
- }
- else if ((pixelToModify == preModificationMaxValue) && (values[index] < preModificationMaxValue))
- {
- recalculateMinMax = true;
- }
- // Modify the correct pixel in our modification buffer.
- pixelToModify = values[index];
- // Potentially expand the min/max range of our image.
- // If these expand, we'll need to recalculate our auto-scale multiplier and offset and refresh the entire image.
- m_minValue = AZStd::min(m_minValue, values[index]);
- m_maxValue = AZStd::max(m_maxValue, values[index]);
- // Track that we've modified the image
- m_imageIsModified = true;
- }
- }
- if (recalculateMinMax)
- {
- // We've potentially shrunk our min/max range, so recalculate the min/max values, multiplier, and offset.
- // We'll do this before the next check, which checks to see if our min/max range has changed.
- SetupAutoScaleMultiplierAndOffset();
- }
- // If our modifications have either expanded or contracted our min/max range, recalculate the scaling multiplier
- // and offset and refresh the entire image.
- if ((preModificationMinValue != m_minValue) || (preModificationMaxValue != m_maxValue))
- {
- SetupMultiplierAndOffset(m_minValue, m_maxValue);
- refreshEntireImage = true;
- }
- }
- else
- {
- // If we're set to manual scale or no scale, modifications are just a simple write loop.
- for (size_t index = 0; index < positions.size(); index++)
- {
- if (PixelIndexIsValid(positions[index]))
- {
- const auto& [x, y] = positions[index];
- // Modify the correct pixel in our modification buffer.
- m_modifiedImageData[(y * width) + x] = values[index];
- // Track that we've modified the image
- m_imageIsModified = true;
- }
- }
- }
- }
- // If we need to refresh the entire image, send the notification outside of the scope block containing the queryMutex lock
- // to avoid any potential deadlocks.
- if (refreshEntireImage)
- {
- LmbrCentral::DependencyNotificationBus::Event(
- GetEntityId(), &LmbrCentral::DependencyNotificationBus::Events::OnCompositionChanged);
- }
- }
- }
|