You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Gems/Atom/Feature/Common/Code/Source/ACES/AcesDisplayMapperFeaturePro...

466 lines
19 KiB
C++

/*
* 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 <Atom/Feature/ACES/AcesDisplayMapperFeatureProcessor.h>
#include <ACES/Aces.h>
#include <Atom/Feature/LookupTable/LookupTableAsset.h>
#include <Atom/RHI/Factory.h>
#include <Atom/RHI/RHISystemInterface.h>
#include <Atom/RPI.Public/Image/ImageSystemInterface.h>
#include <Atom/RPI.Public/Image/StreamingImagePool.h>
#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
#include <AzCore/Debug/EventTrace.h>
#include <AzCore/Debug/Trace.h>
namespace
{
static const AZ::RHI::Format LutFormat = AZ::RHI::Format::R16G16B16A16_FLOAT;
uint16_t ConvertFloatToHalf(const float Value)
{
uint32_t result;
uint32_t uiValue = ((uint32_t*)(&Value))[0];
uint32_t sign = (uiValue & 0x80000000U) >> 16U; // Sign shifted two bytes right for combining with return
uiValue = uiValue & 0x7FFFFFFFU; // Hack off the sign
if (uiValue > 0x47FFEFFFU)
{
// The number is too large to be represented as a half. Saturate to infinity.
result = 0x7FFFU;
}
else
{
if (uiValue < 0x38800000U)
{
// The number is too small to be represented as a normalized half.
// Convert it to a denormalized value.
uint32_t shift = 113U - (uiValue >> 23U);
uiValue = (0x800000U | (uiValue & 0x7FFFFFU)) >> shift;
}
else
{
// Rebias the exponent to represent the value as a normalized half.
uiValue += 0xC8000000U;
}
result = ((uiValue + 0x0FFFU + ((uiValue >> 13U) & 1U)) >> 13U) & 0x7FFFU;
}
// Add back sign and return
return static_cast<uint16_t>(result | sign);
}
}
namespace AZ::Render
{
void AcesDisplayMapperFeatureProcessor::Reflect(ReflectContext* context)
{
if (auto* serializeContext = azrtti_cast<SerializeContext*>(context))
{
serializeContext
->Class<AcesDisplayMapperFeatureProcessor, FeatureProcessor>()
->Version(0);
}
}
void AcesDisplayMapperFeatureProcessor::Activate()
{
GetDefaultDisplayMapperConfiguration(m_displayMapperConfiguration);
}
void AcesDisplayMapperFeatureProcessor::Deactivate()
{
m_ownedLuts.clear();
}
void AcesDisplayMapperFeatureProcessor::Simulate(const FeatureProcessor::SimulatePacket& packet)
{
AZ_TRACE_METHOD();
AZ_UNUSED(packet);
}
void AcesDisplayMapperFeatureProcessor::Render([[maybe_unused]] const FeatureProcessor::RenderPacket& packet)
{
}
void AcesDisplayMapperFeatureProcessor::ApplyLdrOdtParameters(DisplayMapperParameters* displayMapperParameters)
{
AZ_Assert(displayMapperParameters != nullptr, "The pOutParameters must not to be null pointer.");
if (displayMapperParameters == nullptr)
{
return;
}
// These values in the ODT parameter are taken from the reference ACES transform.
//
// The original ACES references.
// Common:
// https://github.com/ampas/aces-dev/blob/master/transforms/ctl/lib/ACESlib.ODT_Common.ctl
// For sRGB:
// https://github.com/ampas/aces-dev/tree/master/transforms/ctl/odt/sRGB
displayMapperParameters->m_cinemaLimits[0] = 0.02f;
displayMapperParameters->m_cinemaLimits[1] = 48.0f;
displayMapperParameters->m_acesSplineParams = GetAcesODTParameters(OutputDeviceTransformType_48Nits);
displayMapperParameters->m_OutputDisplayTransformFlags = AlterSurround | ApplyDesaturation | ApplyCATD60toD65;
displayMapperParameters->m_OutputDisplayTransformMode = Srgb;
ColorConvertionMatrixType colorMatrixType = XYZ_To_Rec709;
switch (displayMapperParameters->m_OutputDisplayTransformMode)
{
case Srgb:
colorMatrixType = XYZ_To_Rec709;
break;
case PerceptualQuantizer:
case Ldr:
colorMatrixType = XYZ_To_Bt2020;
break;
default:
break;
}
displayMapperParameters->m_XYZtoDisplayPrimaries = GetColorConvertionMatrix(colorMatrixType);
displayMapperParameters->m_surroundGamma = 0.9811f;
displayMapperParameters->m_gamma = 2.2f;
}
void AcesDisplayMapperFeatureProcessor::ApplyHdrOdtParameters(DisplayMapperParameters* displayMapperParameters, const OutputDeviceTransformType& odtType)
{
AZ_Assert(displayMapperParameters != nullptr, "The pOutParameters must not to be null pointer.");
if (displayMapperParameters == nullptr)
{
return;
}
// Dynamic range limit values taken from NVIDIA HDR sample.
// These values represent and low and high end of the dynamic range in terms of stops from middle grey (0.18)
float lowerDynamicRangeInStops = -12.f;
float higherDynamicRangeInStops = 10.f;
const float MIDDLE_GREY = 0.18f;
switch (odtType)
{
case OutputDeviceTransformType_1000Nits:
higherDynamicRangeInStops = 10.f;
break;
case OutputDeviceTransformType_2000Nits:
higherDynamicRangeInStops = 11.f;
break;
case OutputDeviceTransformType_4000Nits:
higherDynamicRangeInStops = 12.f;
break;
default:
AZ_Assert(false, "Invalid output device transform type.");
break;
}
displayMapperParameters->m_cinemaLimits[0] = MIDDLE_GREY * exp2(lowerDynamicRangeInStops);
displayMapperParameters->m_cinemaLimits[1] = MIDDLE_GREY * exp2(higherDynamicRangeInStops);
displayMapperParameters->m_acesSplineParams = GetAcesODTParameters(odtType);
displayMapperParameters->m_OutputDisplayTransformFlags = AlterSurround | ApplyDesaturation | ApplyCATD60toD65;
displayMapperParameters->m_OutputDisplayTransformMode = PerceptualQuantizer;
ColorConvertionMatrixType colorMatrixType = XYZ_To_Bt2020;
displayMapperParameters->m_XYZtoDisplayPrimaries = GetColorConvertionMatrix(colorMatrixType);
// Surround gamma value is from the dim surround gamma from the ACES reference transforms.
// https://github.com/ampas/aces-dev/blob/master/transforms/ctl/lib/ACESlib.ODT_Common.ctl
displayMapperParameters->m_surroundGamma = 0.9811f;
displayMapperParameters->m_gamma = 1.0f; // gamma not used with perceptual quantizer, but just set to 1.0 anyways
}
OutputDeviceTransformType AcesDisplayMapperFeatureProcessor::GetOutputDeviceTransformType(RHI::Format bufferFormat)
{
OutputDeviceTransformType outputDeviceTransformType = OutputDeviceTransformType_48Nits;
if (bufferFormat == RHI::Format::R8G8B8A8_UNORM ||
bufferFormat == RHI::Format::B8G8R8A8_UNORM)
{
outputDeviceTransformType = OutputDeviceTransformType_48Nits;
}
else if (bufferFormat == RHI::Format::R10G10B10A2_UNORM)
{
outputDeviceTransformType = OutputDeviceTransformType_1000Nits;
}
else
{
AZ_Assert(false, "Not yet supported.");
// To work normally on unsupported environment, initialize the display parameters by OutputDeviceTransformType_48Nits.
outputDeviceTransformType = OutputDeviceTransformType_48Nits;
}
return outputDeviceTransformType;
}
void AcesDisplayMapperFeatureProcessor::GetAcesDisplayMapperParameters(DisplayMapperParameters* displayMapperParameters, OutputDeviceTransformType odtType)
{
switch (odtType)
{
case OutputDeviceTransformType_48Nits:
ApplyLdrOdtParameters(displayMapperParameters);
break;
case OutputDeviceTransformType_1000Nits:
case OutputDeviceTransformType_2000Nits:
case OutputDeviceTransformType_4000Nits:
ApplyHdrOdtParameters(displayMapperParameters, odtType);
break;
default:
AZ_Assert(false, "This ODT type[%d] is not supported.", odtType);
break;
}
}
void AcesDisplayMapperFeatureProcessor::GetOwnedLut(DisplayMapperLut& displayMapperLut, const AZ::Name& lutName)
{
auto it = m_ownedLuts.find(lutName);
if (it == m_ownedLuts.end())
{
InitializeLutImage(lutName);
it = m_ownedLuts.find(lutName);
AZ_Assert(it != m_ownedLuts.end(), "AcesDisplayMapperFeatureProcessor unable to create LUT %s", lutName.GetCStr());
}
displayMapperLut = it->second;
}
void AcesDisplayMapperFeatureProcessor::GetDisplayMapperLut(DisplayMapperLut& displayMapperLut)
{
const AZ::Name acesLutName("AcesLutImage");
auto it = m_ownedLuts.find(acesLutName);
if (it == m_ownedLuts.end())
{
InitializeLutImage(acesLutName);
it = m_ownedLuts.find(acesLutName);
AZ_Assert(it != m_ownedLuts.end(), "AcesDisplayMapperFeatureProcessor unable to create ACES LUT image");
}
displayMapperLut = it->second;
}
void AcesDisplayMapperFeatureProcessor::GetLutFromAssetLocation(DisplayMapperAssetLut& displayMapperAssetLut, const AZStd::string& assetPath)
{
Data::AssetId assetId = RPI::AssetUtils::GetAssetIdForProductPath(assetPath.c_str(), RPI::AssetUtils::TraceLevel::Error);
GetLutFromAssetId(displayMapperAssetLut, assetId);
}
void AcesDisplayMapperFeatureProcessor::GetLutFromAssetId(DisplayMapperAssetLut& displayMapperAssetLut, const AZ::Data::AssetId assetId)
{
if (!assetId.IsValid())
{
return;
}
// Check first if this already exists
auto it = m_assetLuts.find(assetId.ToString<AZStd::string>());
if (it != m_assetLuts.end())
{
displayMapperAssetLut = it->second;
return;
}
// Read the lut which is a .3dl file embedded within an azasset file.
Data::Asset<RPI::AnyAsset> asset = RPI::AssetUtils::LoadAssetById<RPI::AnyAsset>(assetId, RPI::AssetUtils::TraceLevel::Error);
const LookupTableAsset* lutAsset = RPI::GetDataFromAnyAsset<LookupTableAsset>(asset);
if (lutAsset == nullptr)
{
AZ_Error("AcesDisplayMapperFeatureProcessor", false, "Unable to read LUT from asset.");
asset.Release();
return;
}
// The first row of numbers in a 3dl file is a number of vertices that partition the space from [0,..1023]
// This assumes that the vertices are evenly spaced apart. Non-uniform spacing is supported by the format,
// but haven't been encountered yet.
const size_t lutSize = lutAsset->m_intervals.size();
if (lutSize == 0)
{
AZ_Error("AcesDisplayMapperFeatureProcessor", false, "Lut asset has invalid size.");
asset.Release();
return;
}
// Create a buffer of half floats from the LUT and use it to initialize a 3d texture.
const size_t kChannels = 4;
const size_t kChannelBytes = 2;
const size_t bytesPerRow = lutSize * kChannels * kChannelBytes;
const size_t bytesPerSlice = bytesPerRow * lutSize;
AZStd::vector<uint16_t> u16Buffer;
const size_t bufferSize = lutSize * lutSize * lutSize * kChannels;
u16Buffer.resize(bufferSize);
for (size_t slice = 0; slice < lutSize; slice++)
{
for (size_t column = 0; column < lutSize; column++)
{
for (size_t row = 0; row < lutSize; row++)
{
// Index in the LUT texture data
size_t idx = (column * kChannels) +
((bytesPerRow * row) / kChannelBytes) +
((bytesPerSlice * slice) / kChannelBytes);
// Vertices the .3dl file are listed first by increasing slice, then row, and finally column coordinate
// This corresponds to blue, green, and red channels, respectively.
size_t assetIdx = slice + lutSize * row + (lutSize * lutSize * column);
AZ::u64 red = lutAsset->m_values[assetIdx * 3 + 0];
AZ::u64 green = lutAsset->m_values[assetIdx * 3 + 1];
AZ::u64 blue = lutAsset->m_values[assetIdx * 3 + 2];
// The vertices in the file are given as a positive integer value in [0,..4095] and need to be normalized
constexpr float NormalizeValue = 4095.0f;
u16Buffer[idx + 0] = ConvertFloatToHalf(static_cast<float>(red) / NormalizeValue);
u16Buffer[idx + 1] = ConvertFloatToHalf(static_cast<float>(green) / NormalizeValue);
u16Buffer[idx + 2] = ConvertFloatToHalf(static_cast<float>(blue) / NormalizeValue);
u16Buffer[idx + 3] = 0x3b00; // 1.0 in half
}
}
}
asset.Release();
Data::Instance<RPI::StreamingImagePool> streamingImagePool = RPI::ImageSystemInterface::Get()->GetSystemStreamingPool();
RHI::Size imageSize;
imageSize.m_width = static_cast<uint32_t>(lutSize);
imageSize.m_height = static_cast<uint32_t>(lutSize);
imageSize.m_depth = static_cast<uint32_t>(lutSize);
size_t imageDataSize = bytesPerSlice * lutSize;
Data::Instance<RPI::StreamingImage> lutStreamingImage = RPI::StreamingImage::CreateFromCpuData(
*streamingImagePool, RHI::ImageDimension::Image3D, imageSize, LutFormat, u16Buffer.data(), imageDataSize);
AZ_Error("AcesDisplayMapperFeatureProcessor", lutStreamingImage, "Failed to initialize the lut assetId %s.", assetId.ToString<AZStd::string>().c_str());
DisplayMapperAssetLut assetLut;
assetLut.m_lutStreamingImage = lutStreamingImage;
// Add to the list of LUT asset resources
m_assetLuts.insert(AZStd::pair<AZStd::string, DisplayMapperAssetLut>(assetId.ToString<AZStd::string>(), assetLut));
displayMapperAssetLut = assetLut;
}
void AcesDisplayMapperFeatureProcessor::InitializeImagePool()
{
AZ::RHI::Factory& factory = RHI::Factory::Get();
m_displayMapperImagePool = factory.CreateImagePool();
m_displayMapperImagePool->SetName(Name("DisplayMapperImagePool"));
RHI::ImagePoolDescriptor imagePoolDesc = {};
imagePoolDesc.m_bindFlags = RHI::ImageBindFlags::ShaderReadWrite;
imagePoolDesc.m_budgetInBytes = ImagePoolBudget;
RHI::Device* device = RHI::RHISystemInterface::Get()->GetDevice();
RHI::ResultCode resultCode = m_displayMapperImagePool->Init(*device, imagePoolDesc);
if (resultCode != RHI::ResultCode::Success)
{
AZ_Error("AcesDisplayMapperFeatureProcessor", false, "Failed to initialize image pool.");
return;
}
}
void AcesDisplayMapperFeatureProcessor::InitializeLutImage(const AZ::Name& lutName)
{
if (!m_displayMapperImagePool)
{
InitializeImagePool();
}
DisplayMapperLut lutResource;
lutResource.m_lutImage = RHI::Factory::Get().CreateImage();
lutResource.m_lutImage->SetName(lutName);
RHI::ImageInitRequest imageRequest;
imageRequest.m_image = lutResource.m_lutImage.get();
static const int LutSize = 32;
imageRequest.m_descriptor = RHI::ImageDescriptor::Create3D(RHI::ImageBindFlags::ShaderReadWrite, LutSize, LutSize, LutSize, LutFormat);
RHI::ResultCode resultCode = m_displayMapperImagePool->InitImage(imageRequest);
if (resultCode != RHI::ResultCode::Success)
{
AZ_Error("AcesDisplayMapperFeatureProcessor", false, "Failed to initialize LUT image.");
return;
}
lutResource.m_lutImageViewDescriptor = RHI::ImageViewDescriptor::Create(LutFormat, 0, 0);
lutResource.m_lutImageView = lutResource.m_lutImage->GetImageView(lutResource.m_lutImageViewDescriptor);
if (!lutResource.m_lutImageView.get())
{
AZ_Error("AcesDisplayMapperFeatureProcessor", false, "Failed to initialize LUT image view.");
return;
}
// Add to the list of lut resources
lutResource.m_lutImageView->SetName(lutName);
m_ownedLuts[lutName] = lutResource;
}
ShaperParams AcesDisplayMapperFeatureProcessor::GetShaperParameters(ShaperPresetType shaperPreset, float customMinEv, float customMaxEv)
{
// Default is a linear shaper with bias 0.0 and scale 1.0. That is, fx = x*1.0 + 0.0
ShaperParams shaperParams = { ShaperType::Linear, 0.0, 1.f };
switch (shaperPreset)
{
case ShaperPresetType::None:
break;
case ShaperPresetType::Log2_48Nits:
shaperParams = GetAcesShaperParameters(OutputDeviceTransformType::OutputDeviceTransformType_48Nits);
break;
case ShaperPresetType::Log2_1000Nits:
shaperParams = GetAcesShaperParameters(OutputDeviceTransformType::OutputDeviceTransformType_1000Nits);
break;
case ShaperPresetType::Log2_2000Nits:
shaperParams = GetAcesShaperParameters(OutputDeviceTransformType::OutputDeviceTransformType_2000Nits);
break;
case ShaperPresetType::Log2_4000Nits:
shaperParams = GetAcesShaperParameters(OutputDeviceTransformType::OutputDeviceTransformType_4000Nits);
break;
case ShaperPresetType::LinearCustomRange:
{
// Map the range min exposure - max exposure to 0-1. Convert EV values to linear values here to avoid that work in the shader.
// Shader equation becomes (x - bias) / scale;
constexpr float MediumGray = 0.18f;
const float minValue = MediumGray * powf(2, customMinEv);
const float maxValue = MediumGray * powf(2, customMaxEv);
shaperParams.m_type = ShaperType::Linear;
shaperParams.m_scale = 1.0f / (maxValue - minValue);
shaperParams.m_bias = -minValue * shaperParams.m_scale;
break;
}
case ShaperPresetType::Log2CustomRange:
shaperParams = GetLog2ShaperParameters(customMinEv, customMaxEv);
break;
case ShaperPresetType::PqSmpteSt2084:
shaperParams.m_type = ShaperType::PqSmpteSt2084;
break;
default:
AZ_Error("DisplayMapperPass", false, "Invalid shaper preset type.");
break;
}
return shaperParams;
}
void AcesDisplayMapperFeatureProcessor::GetDefaultDisplayMapperConfiguration(DisplayMapperConfigurationDescriptor& config)
{
// Default configuration is ACES with LDR color grading LUT disabled.
config.m_operationType = DisplayMapperOperationType::Aces;
config.m_ldrGradingLutEnabled = false;
config.m_ldrColorGradingLut.Release();
}
void AcesDisplayMapperFeatureProcessor::RegisterDisplayMapperConfiguration(const DisplayMapperConfigurationDescriptor& config)
{
m_displayMapperConfiguration = config;
}
DisplayMapperConfigurationDescriptor AcesDisplayMapperFeatureProcessor::GetDisplayMapperConfiguration()
{
return m_displayMapperConfiguration;
}
} // namespace AZ::Render