Overhaul of LookModification (#3282)

* Fixed log2 shaper equations. Added bspline sampling for lut. Added options for custom log2 or linear lut with custom exposure ranges.

Signed-off-by: Ken Pruiksma <pruiksma@amazon.com>

* Added support for PQ shaper. Added shader option & cvar for lut sampling quality. Fixed issues in the blend lut shader that were causing considerable quality loss. No longer always changing to the log2 1000 nit shaper when blending luts - if the source luts all use the same shaper, keep using that shaper in the blended lut.

Signed-off-by: Ken Pruiksma <pruiksma@amazon.com>

* Fixed an integer -> float

Signed-off-by: Ken Pruiksma <pruiksma@amazon.com>

* Minor PR reveiw updates

Signed-off-by: Ken Pruiksma <pruiksma@amazon.com>
This commit is contained in:
Ken Pruiksma
2021-08-18 16:59:34 -05:00
committed by GitHub
parent 7814d7679c
commit 90845313fb
24 changed files with 945 additions and 657 deletions
@@ -75,6 +75,8 @@ UNDISCLOSED.
////////////////////////////////////////////////////////////////////////////////
// Constants
#pragma once
#include <Atom/RPI/Math.azsli>
static const float HALF_MAX = 65504.0f;
@@ -90,7 +92,8 @@ static const float DIM_SURROUND_GAMMA = 0.9811;
enum class ShaperType
{
ShaperLinear,
ShaperLog2
ShaperLog2,
PqSmpteSt2084,
};
////////////////////////////////////////////////////////////////////////////////
@@ -0,0 +1,52 @@
/*
* 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
*
*/
#pragma once
#include <Atom/Features/PostProcessing/Aces.azsli>
// Perceptual quantizer coefficients
static const float PqM1 = 1305.0 / 8192.0;
static const float PqM2 = 2533.0 / 32.0;
static const float PqC1 = 102.0 / 128.0;
static const float PqC2 = 2413.0 / 128.0;
static const float PqC3 = 2392.0 / 128.0;
static const float PqMaxNits = 10000.0;
float3 ShaperToLinear(float3 shaperColor, ShaperType shaperType, float shaperBias, float shaperScale)
{
// Apply the inverse of the shaper function to give the color in the working color space
switch (shaperType)
{
case ShaperType::ShaperLinear:
return (shaperColor - shaperBias) / shaperScale;
case ShaperType::ShaperLog2:
return pow(2.0, (shaperColor - shaperBias) / shaperScale);
case ShaperType::PqSmpteSt2084:
shaperColor = min(shaperColor, 1.0);
return PqMaxNits * pow(max(pow(shaperColor, 1.0 / PqM2) - PqC1, 0.0) / (PqC2 - PqC3 * pow(shaperColor, 1.0 / PqM2)), 1.0 / PqM1);
}
return shaperColor;
}
float3 LinearToShaper(float3 linearColor, ShaperType shaperType, float shaperBias, float shaperScale)
{
// Convert from working color space to lut coordinates by applying the shaper function
switch (shaperType)
{
case ShaperType::ShaperLinear:
return linearColor * shaperScale + shaperBias;
case ShaperType::ShaperLog2:
return log2(linearColor) * shaperScale + shaperBias;
case ShaperType::PqSmpteSt2084:
linearColor = min(linearColor, PqMaxNits);
linearColor = linearColor / PqMaxNits;
return pow((PqC1 + PqC2 * pow(linearColor, PqM1)) / (1.0 + PqC3 * pow(linearColor, PqM1)), PqM2);
}
return linearColor;
}
@@ -11,9 +11,7 @@
#include <Atom/Features/PostProcessing/FullscreenPixelInfo.azsli>
#include <Atom/Features/PostProcessing/FullscreenVertex.azsli>
#include <Atom/Features/PostProcessing/PostProcessUtil.azsli>
static const int SHAPER_LINEAR = 0;
static const int SHAPER_LOG2 = 1;
#include <Atom/Features/PostProcessing/Shapers.azsli>
ShaderResourceGroup PassSrg : SRG_PerPass
{
@@ -41,36 +39,22 @@ PSOutput MainPS(VSOutput IN)
float2 uvCoord = float2(IN.m_texCoord.x, IN.m_texCoord.y);
float3 color = PassSrg::m_colorTexture.Sample(PassSrg::LinearSampler, uvCoord).rgb;
ShaperType shaperType = (ShaperType)PassSrg::m_shaperType;
// Convert from working color space to lut coordinates by applying the shaper function
float3 lutCoordinate = color;
if (PassSrg::m_shaperType == SHAPER_LINEAR)
{
lutCoordinate = color * PassSrg::m_shaperScale + PassSrg::m_shaperBias;
}
else if (PassSrg::m_shaperType == SHAPER_LOG2)
{
lutCoordinate = log2(color) * PassSrg::m_shaperScale + PassSrg::m_shaperBias;
}
float3 lutCoordinate = LinearToShaper(color, shaperType, PassSrg::m_shaperBias, PassSrg::m_shaperScale);
// Adjust coordinate to the domain excluding the outer half texel in all directions
uint3 outputDimensions;
PassSrg::m_lut.GetDimensions(outputDimensions.x, outputDimensions.y, outputDimensions.z);
float3 coordBias = 1.0/(2.0 * outputDimensions);
float3 coordScale = (outputDimensions-1.0)/outputDimensions;
float3 coordBias = 1.0 / (2.0 * outputDimensions);
float3 coordScale = (outputDimensions - 1.0) / outputDimensions;
lutCoordinate = (lutCoordinate * coordScale) + coordBias;
float3 lutColor = PassSrg::m_lut.Sample(PassSrg::LinearSampler, lutCoordinate).rgb;
// Apply the inverse of the shaper function to give the color in the working color space
float3 finalColor = lutColor;
if (PassSrg::m_shaperType == SHAPER_LINEAR)
{
finalColor = (lutColor - PassSrg::m_shaperBias)/PassSrg::m_shaperScale;
}
else if (PassSrg::m_shaperType == SHAPER_LOG2)
{
finalColor = pow(2.0, (lutColor - PassSrg::m_shaperBias)/PassSrg::m_shaperScale);
}
float3 finalColor = ShaperToLinear(lutColor, shaperType, PassSrg::m_shaperBias, PassSrg::m_shaperScale);
OUT.m_color.rgb = finalColor;
OUT.m_color.a = 1.0;
@@ -8,6 +8,7 @@
#include <Atom/Features/SrgSemantics.azsli>
#include <Atom/Features/PostProcessing/Aces.azsli>
#include <Atom/Features/PostProcessing/Shapers.azsli>
ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback
{
@@ -62,40 +63,18 @@ ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback
[[range(0, 4)]]
option uint o_numSourceLuts = 0;
float3 ShaperToLinear(float3 shaperColor, ShaperType shaperType, float shaperBias, float shaperScale)
{
// Apply the inverse of the shaper function to give the color in the working color space
float3 linearColor = shaperColor;
if (shaperType == ShaperType::ShaperLinear)
{
linearColor = (shaperColor - shaperBias)/shaperScale;
}
else if (shaperType == ShaperType::ShaperLog2)
{
linearColor = pow(2.0, (shaperColor - shaperBias)/shaperScale);
}
return linearColor;
}
float3 LinearToShaper(float3 linearColor, ShaperType shaperType, float shaperBias, float shaperScale)
{
// Convert from working color space to lut coordinates by applying the shaper function
float3 shaperColor = linearColor;
if (shaperType == ShaperType::ShaperLinear)
{
shaperColor = linearColor * shaperScale + shaperBias;
}
else if (shaperType == ShaperType::ShaperLog2)
{
shaperColor = log2(linearColor) * shaperScale + shaperBias;
}
return shaperColor;
}
float3 GetSourceLutLinearColor(float3 baseColor, Texture3D<float4> sourceLut, ShaperType shaperType, float shaperBias, float shaperScale)
{
// Convert from reference linearColor to the lutCoordinate for this Lut
float3 lutCoord = LinearToShaper(baseColor, shaperType, shaperBias, shaperScale);
// Adjust coordinate to the domain excluding the outer half texel in all directions
uint3 outputDimensions;
sourceLut.GetDimensions(outputDimensions.x, outputDimensions.y, outputDimensions.z);
float3 coordBias = 1.0 / (2.0 * outputDimensions);
float3 coordScale = (outputDimensions - 1.0) / outputDimensions;
lutCoord = (lutCoord * coordScale) + coordBias;
float3 lutColor = sourceLut.SampleLevel(PassSrg::LinearSampler, lutCoord, 0).rgb;
// Convert to linear
float3 linearColor = ShaperToLinear(lutColor, shaperType, shaperBias, shaperScale);
@@ -115,11 +94,7 @@ void MainCS(uint3 dispatch_id: SV_DispatchThreadID)
}
// Get coordinates within the blended LUT 3D texture
float3 baseCoord = float3 (
(float)(dispatch_id.x)/(float)PassSrg::m_blendedLutDimensions.x,
(float)(dispatch_id.y)/(float)PassSrg::m_blendedLutDimensions.y,
(float)(dispatch_id.z)/(float)PassSrg::m_blendedLutDimensions.z
);
float3 baseCoord = float3(outPixel) / float3(PassSrg::m_blendedLutDimensions - 1.0);
// Convert to the base linear color (this is the color of the identity LUT)
float3 baseColor = ShaperToLinear(baseCoord, (ShaperType)PassSrg::m_blendedLutShaperType, PassSrg::m_blendedLutShaperBias, PassSrg::m_blendedLutShaperScale);
@@ -12,6 +12,7 @@
#include <Atom/Features/PostProcessing/FullscreenPixelInfo.azsli>
#include <Atom/Features/PostProcessing/FullscreenVertex.azsli>
#include <Atom/Features/PostProcessing/PostProcessUtil.azsli>
#include <Atom/Features/PostProcessing/Shapers.azsli>
#include "EyeAdaptationUtil.azsli"
ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback
@@ -42,12 +43,111 @@ option bool o_enableExposureControlFeature = false;
// Option shader variable to enable color grading LUT.
option bool o_enableColorGradingLut = false;
// Controls the sampling quality of the blended LUT. Setting this higher can improve the quality of particularly tricky luts.
// 0 - linear
// 1 - 7 tap b-spline
// 2 - 19 tap b-spline
[[range(0, 2)]]
option uint o_lutSampleQuality = 0;
// Sample a 3dtexture with a 7 or 19 tap B-Spline. Consider ripping this out and putting in a more general location.
// This function samples a 4x4x4 neighborhood around the uv. Normally this would take 64 samples, but by taking
// advantage of bilinear filtering this can be done with 27 taps on the edges between pixels. The cost is further
// reduced by dropping either the 8 corners (19 total taps) or also dropping the 12 edges (7 total taps).
float4 SampleBSpline3D(Texture3D<float4> texture, SamplerState linearSampler, float3 uv, float3 textureSize, float3 rcpTextureSize)
{
// Think of sample locations in the 4x4 neighborhood as having a top left coordinate of 0,0 and
// a bottom right coordinate of 3,3.
// Find the position in texture space then round it to get the center of the 1,1 pixel (tc1)
float3 texelPos = uv * textureSize;
float3 tc1= floor(texelPos - 0.5) + 0.5;
// Offset from center position to texel
float3 f = texelPos - tc1;
// Compute B-Spline weights based on the offset
float3 OneMinusF = (1.0 - f);
float3 OneMinusF2 = OneMinusF * OneMinusF;
float3 OneMinusF3 = OneMinusF2 * OneMinusF;
float3 w0 = OneMinusF3;
float3 w1 = 4.0 + 3.0 * f * f * f - 6.0 * f * f;
float3 w2 = 4.0 + 3.0 * OneMinusF3 - 6.0 * OneMinusF2;
float3 w3 = f * f * f;
float3 w12 = w1 + w2;
// Compute uv coordinates for sampling the texture
float3 tc0 = (tc1 - 1.0f) * rcpTextureSize;
float3 tc3 = (tc1 + 2.0f) * rcpTextureSize;
float3 tc12 = (tc1 + w2 / w12) * rcpTextureSize;
// Compute sample weights
float sw0 = w12.x * w0.y * w12.z;
float sw1 = w0.x * w12.y * w12.z;
float sw2 = w12.x * w12.y * w12.z;
float sw3 = w3.x * w12.y * w12.z;
float sw4 = w12.x * w3.y * w12.z;
float sw5 = w12.x * w12.y * w0.z;
float sw6 = w12.x * w12.y * w3.z;
// total weight of samples to normalize result.
float totalWeight = sw0 + sw1 + sw2 + sw3 + sw4 + sw5 + sw6;
float4 result = 0.0f;
result += texture.SampleLevel(linearSampler, float3(tc12.x, tc0.y, tc12.z), 0.0) * sw0;
result += texture.SampleLevel(linearSampler, float3( tc0.x, tc12.y, tc12.z), 0.0) * sw1;
result += texture.SampleLevel(linearSampler, float3(tc12.x, tc12.y, tc12.z), 0.0) * sw2;
result += texture.SampleLevel(linearSampler, float3( tc3.x, tc12.y, tc12.z), 0.0) * sw3;
result += texture.SampleLevel(linearSampler, float3(tc12.x, tc3.y, tc12.z), 0.0) * sw4;
result += texture.SampleLevel(linearSampler, float3(tc12.x, tc12.y, tc0.z), 0.0) * sw5;
result += texture.SampleLevel(linearSampler, float3(tc12.x, tc12.y, tc3.z), 0.0) * sw6;
if (o_lutSampleQuality == 2)
{
// Extra 12 taps for Diagonals to increase the quality further.
float sw7 = w0.x * w0.y * w12.z;
float sw8 = w0.x * w3.y * w12.z;
float sw9 = w3.x * w0.y * w12.z;
float sw10 = w3.x * w3.y * w12.z;
float sw11 = w12.x * w0.y * w0.z;
float sw12 = w12.x * w0.y * w3.z;
float sw13 = w12.x * w3.y * w0.z;
float sw14 = w12.x * w3.y * w3.z;
float sw15 = w0.x * w12.y * w0.z;
float sw16 = w0.x * w12.y * w3.z;
float sw17 = w3.x * w12.y * w0.z;
float sw18 = w3.x * w12.y * w3.z;
totalWeight += sw7 + sw8 + sw9 + sw10 + sw11 + sw12 + sw13 + sw14 + sw15 + sw16 + sw17 + sw18;
result += texture.SampleLevel(linearSampler, float3(tc0.x, tc0.y, tc12.z), 0.0) * sw7;
result += texture.SampleLevel(linearSampler, float3(tc0.x, tc3.y, tc12.z), 0.0) * sw8;
result += texture.SampleLevel(linearSampler, float3(tc3.x, tc0.y, tc12.z), 0.0) * sw9;
result += texture.SampleLevel(linearSampler, float3(tc3.x, tc3.y, tc12.z), 0.0) * sw10;
result += texture.SampleLevel(linearSampler, float3(tc12.x, tc0.y, tc0.z), 0.0) * sw11;
result += texture.SampleLevel(linearSampler, float3(tc12.x, tc0.y, tc3.z), 0.0) * sw12;
result += texture.SampleLevel(linearSampler, float3(tc12.x, tc3.y, tc0.z), 0.0) * sw13;
result += texture.SampleLevel(linearSampler, float3(tc12.x, tc3.y, tc3.z), 0.0) * sw14;
result += texture.SampleLevel(linearSampler, float3(tc0.x, tc12.y, tc0.z), 0.0) * sw15;
result += texture.SampleLevel(linearSampler, float3(tc0.x, tc12.y, tc3.z), 0.0) * sw16;
result += texture.SampleLevel(linearSampler, float3(tc3.x, tc12.y, tc0.z), 0.0) * sw17;
result += texture.SampleLevel(linearSampler, float3(tc3.x, tc12.y, tc3.z), 0.0) * sw18;
}
return result / totalWeight;
}
PSOutput MainPS(VSOutput IN)
{
PSOutput OUT;
// Fetch the pixel color from the input texture
float3 color = PassSrg::m_framebuffer.Sample(PassSrg::LinearSampler, IN.m_texCoord).rgb;
float3 color = PassSrg::m_framebuffer.SampleLevel(PassSrg::LinearSampler, IN.m_texCoord, 0.0).rgb;
if (o_enableExposureControlFeature)
{
@@ -63,36 +163,28 @@ PSOutput MainPS(VSOutput IN)
if (o_enableColorGradingLut)
{
// Convert from working color space to lut coordinates by applying the shaper function
float3 lutCoordinate = color;
if (shaperType == ShaperType::ShaperLinear)
{
lutCoordinate = color * PassSrg::m_shaperScale + PassSrg::m_shaperBias;
}
else if (shaperType == ShaperType::ShaperLog2)
{
lutCoordinate = log2(color) * PassSrg::m_shaperScale + PassSrg::m_shaperBias;
}
float3 lutCoordinate = LinearToShaper(color, shaperType, PassSrg::m_shaperBias, PassSrg::m_shaperScale);
// Adjust coordinate to the domain excluding the outer half texel in all directions
uint3 outputDimensions;
PassSrg::m_gradingLut.GetDimensions(outputDimensions.x, outputDimensions.y, outputDimensions.z);
float3 coordBias = 0.5f / outputDimensions;
float3 coordScale = (outputDimensions-1.0)/outputDimensions;
float3 sizeMinusOne = outputDimensions - 1.0;
float3 coordScale = sizeMinusOne / outputDimensions;
lutCoordinate = (lutCoordinate * coordScale) + coordBias;
float3 lutColor = PassSrg::m_gradingLut.Sample(PassSrg::LinearSampler, lutCoordinate).rgb;
float3 lutColor = float3(0.0, 0.0, 0.0);
if (o_lutSampleQuality == 0)
{
lutColor = PassSrg::m_gradingLut.SampleLevel(PassSrg::LinearSampler, lutCoordinate, 0.0).rgb;
}
else
{
lutColor = SampleBSpline3D(PassSrg::m_gradingLut, PassSrg::LinearSampler, lutCoordinate, float3(outputDimensions), 1.0 / float3(outputDimensions)).rgb;
}
// Apply the inverse of the shaper function to give the color in the working color space
float3 finalColor = lutColor;
if (shaperType == ShaperType::ShaperLinear)
{
finalColor = (lutColor - PassSrg::m_shaperBias)/PassSrg::m_shaperScale;
}
else if (shaperType == ShaperType::ShaperLog2)
{
finalColor = pow(2.0, (lutColor - PassSrg::m_shaperBias)/PassSrg::m_shaperScale);
}
color = finalColor;
color = ShaperToLinear(lutColor, shaperType, PassSrg::m_shaperBias, PassSrg::m_shaperScale);
}
OUT.m_color.rgb = color;
@@ -1,9 +1,13 @@
{
"Shader" : "LookModificationTransform.shader",
"Variants" : [
{ "StableId": 1, "Options" : { "o_enableExposureControlFeature": "true", "o_enableColorGradingLut": "true" } },
{ "StableId": 2, "Options" : { "o_enableExposureControlFeature": "true", "o_enableColorGradingLut": "false" } },
{ "StableId": 3, "Options" : { "o_enableExposureControlFeature": "false", "o_enableColorGradingLut": "true" } },
{ "StableId": 4, "Options" : { "o_enableExposureControlFeature": "false", "o_enableColorGradingLut": "false" } }
{ "StableId": 1, "Options" : { "o_enableExposureControlFeature": "true", "o_enableColorGradingLut": "false" } },
{ "StableId": 2, "Options" : { "o_enableExposureControlFeature": "false", "o_enableColorGradingLut": "false" } },
{ "StableId": 3, "Options" : { "o_enableExposureControlFeature": "true", "o_enableColorGradingLut": "true", "o_lutSampleQuality": 0 } },
{ "StableId": 4, "Options" : { "o_enableExposureControlFeature": "false", "o_enableColorGradingLut": "true", "o_lutSampleQuality": 0 } },
{ "StableId": 5, "Options" : { "o_enableExposureControlFeature": "true", "o_enableColorGradingLut": "true", "o_lutSampleQuality": 1 } },
{ "StableId": 6, "Options" : { "o_enableExposureControlFeature": "false", "o_enableColorGradingLut": "true", "o_lutSampleQuality": 1 } },
{ "StableId": 7, "Options" : { "o_enableExposureControlFeature": "true", "o_enableColorGradingLut": "true", "o_lutSampleQuality": 2 } },
{ "StableId": 8, "Options" : { "o_enableExposureControlFeature": "false", "o_enableColorGradingLut": "true", "o_lutSampleQuality": 2 } }
]
}
+17 -27
View File
@@ -203,47 +203,37 @@ namespace AZ
return ODT_48nits;
}
ShaperParams GetLog2ShaperParameters(float minStops, float maxStops)
{
ShaperParams shaperParams;
constexpr float Log2MediumGray = -2.47393118833f; // log2f(0.18f);
shaperParams.m_type = ShaperType::Log2;
shaperParams.m_scale = 1.0f / (maxStops - minStops);
shaperParams.m_bias = -((minStops + Log2MediumGray) * shaperParams.m_scale);
return shaperParams;
}
ShaperParams GetAcesShaperParameters(OutputDeviceTransformType odtType)
{
AZ_Assert(static_cast<uint32_t>(odtType) < static_cast<uint32_t>(NumOutputDeviceTransformTypes), "Invalid ODT type specified.");
ShaperParams shaperParams;
// These values represent and low and high end of the dynamic range in terms of stops from middle grey (0.18)
float lowerDynamicRangeInStops;
float higherDynamicRangeInStops;
const float MIDDLE_GREY = 0.18f;
switch (odtType)
{
case OutputDeviceTransformType_48Nits:
lowerDynamicRangeInStops = -6.5f;
higherDynamicRangeInStops = 6.5f;
break;
return GetLog2ShaperParameters(-6.5f, 6.5f);
case OutputDeviceTransformType_1000Nits:
lowerDynamicRangeInStops = -12.f;
higherDynamicRangeInStops = 10.f;
break;
return GetLog2ShaperParameters(-12.0f, 10.0f);
case OutputDeviceTransformType_2000Nits:
lowerDynamicRangeInStops = -12.f;
higherDynamicRangeInStops = 11.f;
break;
return GetLog2ShaperParameters(-12.0f, 11.0f);
case OutputDeviceTransformType_4000Nits:
lowerDynamicRangeInStops = -12.f;
higherDynamicRangeInStops = 12.f;
break;
return GetLog2ShaperParameters(-12.0f, 12.0f);
default:
AZ_Assert(false, "Invalid output device transform type.");
return shaperParams;
break;
}
float logMin = log2(MIDDLE_GREY * exp2(lowerDynamicRangeInStops));
float logMax = log2(MIDDLE_GREY * exp2(higherDynamicRangeInStops));
shaperParams.scale = 1.0f / (logMax - logMin);
shaperParams.bias = -shaperParams.scale * logMin;
shaperParams.type = ShaperType::Log2;
return shaperParams;
return ShaperParams();
}
Matrix3x3 GetColorConvertionMatrix(ColorConvertionMatrixType type)
+14 -8
View File
@@ -124,18 +124,19 @@ namespace AZ
NumColorConvertionMatrixTypes
};
enum ShaperType
enum class ShaperType : uint32_t
{
Linear = 0,
Log2 = 1,
PqSmpteSt2084 = 2,
NumShaperTypes
};
struct ShaperParams
{
ShaperType type = ShaperType::Linear;
float bias = 0.f;
float scale = 1.f;
ShaperType m_type = ShaperType::Linear;
float m_bias = 0.0f;
float m_scale = 1.0f;
};
enum class DisplayMapperOperationType : uint32_t
@@ -151,10 +152,14 @@ namespace AZ
enum class ShaperPresetType
{
None = 0,
Log2_48_nits,
Log2_1000_nits,
Log2_2000_nits,
Log2_4000_nits
LinearCustomRange,
Log2_48Nits,
Log2_1000Nits,
Log2_2000Nits,
Log2_4000Nits,
Log2CustomRange,
PqSmpteSt2084,
NumShaperTypes
};
enum class ToneMapperType
@@ -171,6 +176,7 @@ namespace AZ
};
SegmentedSplineParamsC9 GetAcesODTParameters(OutputDeviceTransformType odtType);
ShaperParams GetLog2ShaperParameters(float minStops, float maxStops);
ShaperParams GetAcesShaperParameters(OutputDeviceTransformType odtType);
Matrix3x3 GetColorConvertionMatrix(ColorConvertionMatrixType type);
@@ -78,7 +78,7 @@ namespace AZ
static OutputDeviceTransformType GetOutputDeviceTransformType(RHI::Format bufferFormat);
static void GetAcesDisplayMapperParameters(DisplayMapperParameters* displayMapperParameters, OutputDeviceTransformType odtType);
static ShaperParams GetShaperParameters(ShaperPresetType shaperPreset);
static ShaperParams GetShaperParameters(ShaperPresetType shaperPreset, float customMinEv = 0.0f, float customMaxEv = 0.0f);
static void GetDefaultDisplayMapperConfiguration(DisplayMapperConfigurationDescriptor& config);
// DisplayMapperFeatureProcessorInteface overrides...
@@ -102,8 +102,6 @@ namespace AZ
static constexpr const char* FeatureProcessorName = "AcesDisplayMapperFeatureProcessor";
static const int LutSize = 32;
static const RHI::Format LutFormat = RHI::Format::R16G16B16A16_FLOAT;
static const int ImagePoolBudget = 1 << 20; // 1 Megabyte
// LUTs that are baked through shaders
@@ -10,11 +10,9 @@
// PARAM(NAME, MEMBER_NAME, DEFAULT_VALUE, ...)
AZ_GFX_BOOL_PARAM(Enabled, m_enabled, false)
AZ_GFX_COMMON_PARAM(Data::Asset<RPI::AnyAsset>, ColorGradingLut, m_colorGradingLut, {})
AZ_GFX_COMMON_PARAM(AZ::Render::ShaperPresetType, ShaperPresetType, m_shaperPresetType, AZ::Render::ShaperPresetType::Log2_48_nits)
AZ_GFX_COMMON_PARAM(AZ::Render::ShaperPresetType, ShaperPresetType, m_shaperPresetType, AZ::Render::ShaperPresetType::Log2_48Nits)
AZ_GFX_COMMON_PARAM(float, CustomMinExposure, m_customMinExposure, -6.5)
AZ_GFX_COMMON_PARAM(float, CustomMaxExposure, m_customMaxExposure, 6.5)
AZ_GFX_FLOAT_PARAM(ColorGradingLutIntensity, m_colorGradingLutIntensity, 1.0)
AZ_GFX_FLOAT_PARAM(ColorGradingLutOverride, m_colorGradingLutOverride, 1.0)
@@ -21,6 +21,7 @@
namespace
{
static const AZ::RHI::Format LutFormat = AZ::RHI::Format::R16G16B16A16_FLOAT;
uint16_t ConvertFloatToHalf(const float Value)
{
@@ -56,395 +57,409 @@ namespace
}
}
namespace AZ
namespace AZ::Render
{
namespace Render
void AcesDisplayMapperFeatureProcessor::Reflect(ReflectContext* context)
{
void AcesDisplayMapperFeatureProcessor::Reflect(ReflectContext* context)
if (auto* serializeContext = azrtti_cast<SerializeContext*>(context))
{
if (auto* serializeContext = azrtti_cast<SerializeContext*>(context))
{
serializeContext
->Class<AcesDisplayMapperFeatureProcessor, FeatureProcessor>()
->Version(0);
}
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;
}
void AcesDisplayMapperFeatureProcessor::Activate()
// 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)
{
GetDefaultDisplayMapperConfiguration(m_displayMapperConfiguration);
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;
}
void AcesDisplayMapperFeatureProcessor::Deactivate()
// 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)
{
m_ownedLuts.clear();
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;
}
void AcesDisplayMapperFeatureProcessor::Simulate(const FeatureProcessor::SimulatePacket& packet)
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)
{
AZ_TRACE_METHOD();
AZ_UNUSED(packet);
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;
}
void AcesDisplayMapperFeatureProcessor::Render([[maybe_unused]] const FeatureProcessor::RenderPacket& packet)
// Check first if this already exists
auto it = m_assetLuts.find(assetId.ToString<AZStd::string>());
if (it != m_assetLuts.end())
{
displayMapperAssetLut = it->second;
return;
}
void AcesDisplayMapperFeatureProcessor::ApplyLdrOdtParameters(DisplayMapperParameters* displayMapperParameters)
// 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_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;
AZ_Error("AcesDisplayMapperFeatureProcessor", false, "Unable to read LUT from asset.");
asset.Release();
return;
}
void AcesDisplayMapperFeatureProcessor::ApplyHdrOdtParameters(DisplayMapperParameters* displayMapperParameters, const OutputDeviceTransformType& odtType)
// 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_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
AZ_Error("AcesDisplayMapperFeatureProcessor", false, "Lut asset has invalid size.");
asset.Release();
return;
}
OutputDeviceTransformType AcesDisplayMapperFeatureProcessor::GetOutputDeviceTransformType(RHI::Format bufferFormat)
// 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++)
{
OutputDeviceTransformType outputDeviceTransformType = OutputDeviceTransformType_48Nits;
if (bufferFormat == RHI::Format::R8G8B8A8_UNORM ||
bufferFormat == RHI::Format::B8G8R8A8_UNORM)
for (size_t column = 0; column < lutSize; column++)
{
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.
uint32_t lutSize = static_cast<uint32_t>(lutAsset->m_intervals.size());
if (lutSize == 0)
{
AZ_Error("AcesDisplayMapperFeatureProcessor", false, "Lut asset has invalid size.");
asset.Release();
return;
}
// The vertices in the file are given as a positive integer value in [0,..4095] and need to be normalized
// and stored into a linear unaligned buffer used to initialize the streaming image.
const float normalizeValue = 4095.0f;
const int kChannels = 4;
const int kChannelBytes = 2;
int bytesPerRow = lutSize * kChannels * kChannelBytes;
int bytesPerSlice = bytesPerRow * lutSize;
AZStd::vector<uint16_t> u16Buffer;
size_t bufferSize = (bytesPerSlice * lutSize) / sizeof(uint16_t);
u16Buffer.resize(bufferSize);
uint16_t* data = u16Buffer.data();
for (int slice = 0; slice < (int)lutSize; slice++)
{
for (int column = 0; column < (int)lutSize; column++)
for (size_t row = 0; row < lutSize; row++)
{
for (int row = 0; row < (int)lutSize; row++)
{
// Index in the LUT texture data
int idx = (column * kChannels) +
(bytesPerRow * row / sizeof(uint16_t)) +
((bytesPerSlice * slice) / sizeof(uint16_t));
// 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.
int assetIdx = slice + lutSize * row + (lutSize * lutSize * column);
// 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];
data[idx + 0] = ConvertFloatToHalf(static_cast<float>(red) / normalizeValue);
data[idx + 1] = ConvertFloatToHalf(static_cast<float>(green) / normalizeValue);
data[idx + 2] = ConvertFloatToHalf(static_cast<float>(blue) / normalizeValue);
data[idx + 3] = 0x3b00; // 1.0 in half
}
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, 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()
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::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;
}
AZ_Error("AcesDisplayMapperFeatureProcessor", false, "Failed to initialize image pool.");
return;
}
}
void AcesDisplayMapperFeatureProcessor::InitializeLutImage(const AZ::Name& lutName)
void AcesDisplayMapperFeatureProcessor::InitializeLutImage(const AZ::Name& lutName)
{
if (!m_displayMapperImagePool)
{
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();
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;
InitializeImagePool();
}
ShaperParams AcesDisplayMapperFeatureProcessor::GetShaperParameters(ShaperPresetType shaperPreset)
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)
{
// 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 };
OutputDeviceTransformType outputDeviceTransformType = OutputDeviceTransformType::NumOutputDeviceTransformTypes;
switch (shaperPreset)
{
case ShaperPresetType::None:
break;
case ShaperPresetType::Log2_48_nits:
outputDeviceTransformType = OutputDeviceTransformType::OutputDeviceTransformType_48Nits;
break;
case ShaperPresetType::Log2_1000_nits:
outputDeviceTransformType = OutputDeviceTransformType::OutputDeviceTransformType_1000Nits;
break;
case ShaperPresetType::Log2_2000_nits:
outputDeviceTransformType = OutputDeviceTransformType::OutputDeviceTransformType_2000Nits;
break;
case ShaperPresetType::Log2_4000_nits:
outputDeviceTransformType = OutputDeviceTransformType::OutputDeviceTransformType_4000Nits;
break;
default:
AZ_Error("DisplayMapperPass", false, "Invalid shaper preset type.");
break;
}
if (outputDeviceTransformType < OutputDeviceTransformType::NumOutputDeviceTransformTypes)
{
shaperParams = GetAcesShaperParameters(outputDeviceTransformType);
}
return shaperParams;
AZ_Error("AcesDisplayMapperFeatureProcessor", false, "Failed to initialize LUT image.");
return;
}
void AcesDisplayMapperFeatureProcessor::GetDefaultDisplayMapperConfiguration(DisplayMapperConfigurationDescriptor& config)
lutResource.m_lutImageViewDescriptor = RHI::ImageViewDescriptor::Create(LutFormat, 0, 0);
lutResource.m_lutImageView = lutResource.m_lutImage->GetImageView(lutResource.m_lutImageViewDescriptor);
if (!lutResource.m_lutImageView.get())
{
// Default configuration is ACES with LDR color grading LUT disabled.
config.m_operationType = DisplayMapperOperationType::Aces;
config.m_ldrGradingLutEnabled = false;
config.m_ldrColorGradingLut.Release();
AZ_Error("AcesDisplayMapperFeatureProcessor", false, "Failed to initialize LUT image view.");
return;
}
void AcesDisplayMapperFeatureProcessor::RegisterDisplayMapperConfiguration(const DisplayMapperConfigurationDescriptor& config)
{
m_displayMapperConfiguration = config;
}
// Add to the list of lut resources
lutResource.m_lutImageView->SetName(lutName);
m_ownedLuts[lutName] = lutResource;
}
DisplayMapperConfigurationDescriptor AcesDisplayMapperFeatureProcessor::GetDisplayMapperConfiguration()
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)
{
return m_displayMapperConfiguration;
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;
}
} // namespace Render
} // namespace AZ
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
@@ -91,8 +91,8 @@ namespace AZ
m_shaderResourceGroup->SetImageView(m_shaderInputLutImageIndex, m_displayMapperLut.m_lutImageView.get());
}
m_shaderResourceGroup->SetConstant(m_shaderInputShaperBiasIndex, m_shaperParams.bias);
m_shaderResourceGroup->SetConstant(m_shaderInputShaperScaleIndex, m_shaperParams.scale);
m_shaderResourceGroup->SetConstant(m_shaderInputShaperBiasIndex, m_shaperParams.m_bias);
m_shaderResourceGroup->SetConstant(m_shaderInputShaperScaleIndex, m_shaperParams.m_scale);
}
BindPassSrg(context, m_shaderResourceGroup);
@@ -109,9 +109,9 @@ namespace AZ
{
m_shaderResourceGroup->SetImageView(m_shaderInputLutImageIndex, m_lutResource.m_lutStreamingImage->GetImageView());
m_shaderResourceGroup->SetConstant(m_shaderShaperTypeIndex, m_shaperParams.type);
m_shaderResourceGroup->SetConstant(m_shaderShaperBiasIndex, m_shaperParams.bias);
m_shaderResourceGroup->SetConstant(m_shaderShaperScaleIndex, m_shaperParams.scale);
m_shaderResourceGroup->SetConstant(m_shaderShaperTypeIndex, m_shaperParams.m_type);
m_shaderResourceGroup->SetConstant(m_shaderShaperBiasIndex, m_shaperParams.m_bias);
m_shaderResourceGroup->SetConstant(m_shaderShaperScaleIndex, m_shaperParams.m_scale);
}
}
}
@@ -94,8 +94,8 @@ namespace AZ
m_shaderResourceGroup->SetImageView(m_shaderInputLutImageIndex, m_displayMapperLut.m_lutImageView.get());
m_shaderResourceGroup->SetConstant(m_shaderInputShaperBiasIndex, m_shaperParams.bias);
m_shaderResourceGroup->SetConstant(m_shaderInputShaperScaleIndex, m_shaperParams.scale);
m_shaderResourceGroup->SetConstant(m_shaderInputShaperBiasIndex, m_shaperParams.m_bias);
m_shaderResourceGroup->SetConstant(m_shaderInputShaperScaleIndex, m_shaperParams.m_scale);
}
BindPassSrg(context, m_shaderResourceGroup);
@@ -26,6 +26,8 @@ namespace AZ
seed = TypeHash64(m_overrideStrength, seed);
seed = TypeHash64(m_assetId.GetId(), seed);
seed = TypeHash64(m_shaperPreset, seed);
seed = TypeHash64(m_customMinExposure, seed);
seed = TypeHash64(m_customMaxExposure, seed);
return seed;
}
@@ -50,6 +52,9 @@ namespace AZ
lutBlend.m_intensity = GetColorGradingLutIntensity();
lutBlend.m_overrideStrength = GetColorGradingLutOverride() * alpha;
lutBlend.m_assetId = lutAssetId;
lutBlend.m_shaperPreset = GetShaperPresetType();
lutBlend.m_customMinExposure = GetCustomMinExposure();
lutBlend.m_customMaxExposure = GetCustomMaxExposure();
target->AddLutBlend(lutBlend);
}
}
@@ -87,6 +92,9 @@ namespace AZ
blendItem.m_intensity = GetColorGradingLutIntensity();
blendItem.m_overrideStrength = GetColorGradingLutOverride();
blendItem.m_assetId = GetColorGradingLut();
blendItem.m_shaperPreset = GetShaperPresetType();
blendItem.m_customMinExposure = GetCustomMinExposure();
blendItem.m_customMaxExposure = GetCustomMaxExposure();
m_lutBlendStack.insert(m_lutBlendStack.begin(), blendItem);
}
}
@@ -33,7 +33,10 @@ namespace AZ
//! Asset ID of LUT
Data::Asset<RPI::AnyAsset> m_assetId;
//! Shaper preset type
ShaperPresetType m_shaperPreset = AZ::Render::ShaperPresetType::Log2_48_nits;
ShaperPresetType m_shaperPreset = AZ::Render::ShaperPresetType::Log2_48Nits;
//! When shaper preset is custom, these values set min and max exposure.
float m_customMinExposure = -6.5;
float m_customMaxExposure = 6.5;
HashValue64 GetHash(HashValue64 seed) const;
};
@@ -21,7 +21,7 @@ namespace AZ
namespace Render
{
static const char* const NumSourceLutsShaderVariantOptionName{ "o_numSourceLuts" };
RPI::Ptr<BlendColorGradingLutsPass> BlendColorGradingLutsPass::Create(const RPI::PassDescriptor& descriptor)
{
RPI::Ptr<BlendColorGradingLutsPass> pass = aznew BlendColorGradingLutsPass(descriptor);
@@ -151,9 +151,9 @@ namespace AZ
{
m_shaderResourceGroup->SetImageView(m_shaderInputBlendedLutImageIndex, m_blendedLut.m_lutImageView.get());
m_shaderResourceGroup->SetConstant(m_shaderInputBlendedLutDimensionsIndex, m_blendedLutDimensions);
m_shaderResourceGroup->SetConstant(m_shaderInputBlendedLutShaperTypeIndex, m_blendedLutShaperParams.type);
m_shaderResourceGroup->SetConstant(m_shaderInputBlendededLutShaperBiasIndex, m_blendedLutShaperParams.bias);
m_shaderResourceGroup->SetConstant(m_shaderInputBlendededLutShaperScaleIndex, m_blendedLutShaperParams.scale);
m_shaderResourceGroup->SetConstant(m_shaderInputBlendedLutShaperTypeIndex, m_blendedLutShaperParams.m_type);
m_shaderResourceGroup->SetConstant(m_shaderInputBlendededLutShaperBiasIndex, m_blendedLutShaperParams.m_bias);
m_shaderResourceGroup->SetConstant(m_shaderInputBlendededLutShaperScaleIndex, m_blendedLutShaperParams.m_scale);
m_shaderResourceGroup->SetConstant(m_shaderInputWeight0Index, m_weights[0]);
m_shaderResourceGroup->SetConstant(m_shaderInputWeight1Index, m_weights[1]);
m_shaderResourceGroup->SetConstant(m_shaderInputWeight2Index, m_weights[2]);
@@ -163,33 +163,33 @@ namespace AZ
if (m_colorGradingLuts[0].m_lutStreamingImage)
{
m_shaderResourceGroup->SetImageView(m_shaderInputSourceLut1ImageIndex, m_colorGradingLuts[0].m_lutStreamingImage->GetImageView());
m_shaderResourceGroup->SetConstant(m_shaderInputSourceLut1ShaperTypeIndex, m_colorGradingShaperParams[0].type);
m_shaderResourceGroup->SetConstant(m_shaderInputSourceLut1ShaperBiasIndex, m_colorGradingShaperParams[0].bias);
m_shaderResourceGroup->SetConstant(m_shaderInputSourceLut1ShaperScaleIndex, m_colorGradingShaperParams[0].scale);
m_shaderResourceGroup->SetConstant(m_shaderInputSourceLut1ShaperTypeIndex, m_colorGradingShaperParams[0].m_type);
m_shaderResourceGroup->SetConstant(m_shaderInputSourceLut1ShaperBiasIndex, m_colorGradingShaperParams[0].m_bias);
m_shaderResourceGroup->SetConstant(m_shaderInputSourceLut1ShaperScaleIndex, m_colorGradingShaperParams[0].m_scale);
}
if (m_colorGradingLuts[1].m_lutStreamingImage)
{
m_shaderResourceGroup->SetImageView(m_shaderInputSourceLut2ImageIndex, m_colorGradingLuts[1].m_lutStreamingImage->GetImageView());
m_shaderResourceGroup->SetConstant(m_shaderInputSourceLut2ShaperTypeIndex, m_colorGradingShaperParams[1].type);
m_shaderResourceGroup->SetConstant(m_shaderInputSourceLut2ShaperBiasIndex, m_colorGradingShaperParams[1].bias);
m_shaderResourceGroup->SetConstant(m_shaderInputSourceLut2ShaperScaleIndex, m_colorGradingShaperParams[1].scale);
m_shaderResourceGroup->SetConstant(m_shaderInputSourceLut2ShaperTypeIndex, m_colorGradingShaperParams[1].m_type);
m_shaderResourceGroup->SetConstant(m_shaderInputSourceLut2ShaperBiasIndex, m_colorGradingShaperParams[1].m_bias);
m_shaderResourceGroup->SetConstant(m_shaderInputSourceLut2ShaperScaleIndex, m_colorGradingShaperParams[1].m_scale);
}
if (m_colorGradingLuts[2].m_lutStreamingImage)
{
m_shaderResourceGroup->SetImageView(m_shaderInputSourceLut3ImageIndex, m_colorGradingLuts[2].m_lutStreamingImage->GetImageView());
m_shaderResourceGroup->SetConstant(m_shaderInputSourceLut3ShaperTypeIndex, m_colorGradingShaperParams[2].type);
m_shaderResourceGroup->SetConstant(m_shaderInputSourceLut3ShaperBiasIndex, m_colorGradingShaperParams[2].bias);
m_shaderResourceGroup->SetConstant(m_shaderInputSourceLut3ShaperScaleIndex, m_colorGradingShaperParams[2].scale);
m_shaderResourceGroup->SetConstant(m_shaderInputSourceLut3ShaperTypeIndex, m_colorGradingShaperParams[2].m_type);
m_shaderResourceGroup->SetConstant(m_shaderInputSourceLut3ShaperBiasIndex, m_colorGradingShaperParams[2].m_bias);
m_shaderResourceGroup->SetConstant(m_shaderInputSourceLut3ShaperScaleIndex, m_colorGradingShaperParams[2].m_scale);
}
if (m_colorGradingLuts[3].m_lutStreamingImage)
{
m_shaderResourceGroup->SetImageView(m_shaderInputSourceLut4ImageIndex, m_colorGradingLuts[3].m_lutStreamingImage->GetImageView());
m_shaderResourceGroup->SetConstant(m_shaderInputSourceLut4ShaperTypeIndex, m_colorGradingShaperParams[3].type);
m_shaderResourceGroup->SetConstant(m_shaderInputSourceLut4ShaperBiasIndex, m_colorGradingShaperParams[3].bias);
m_shaderResourceGroup->SetConstant(m_shaderInputSourceLut4ShaperScaleIndex, m_colorGradingShaperParams[3].scale);
m_shaderResourceGroup->SetConstant(m_shaderInputSourceLut4ShaperTypeIndex, m_colorGradingShaperParams[3].m_type);
m_shaderResourceGroup->SetConstant(m_shaderInputSourceLut4ShaperBiasIndex, m_colorGradingShaperParams[3].m_bias);
m_shaderResourceGroup->SetConstant(m_shaderInputSourceLut4ShaperScaleIndex, m_colorGradingShaperParams[3].m_scale);
}
if (m_shaderResourceGroup->HasShaderVariantKeyFallbackEntry())
@@ -244,7 +244,170 @@ namespace AZ
m_blendedLutShaperParams = shaperParams;
}
AZStd::optional<ShaperParams> BlendColorGradingLutsPass::GetCommonShaperParams() const
{
LookModificationSettings* settings = GetLookModificationSettings();
if (settings)
{
settings->PrepareLutBlending();
ShaperPresetType type = ShaperPresetType::NumShaperTypes;
float customMinExposure = 0.0;
float customMaxExposure = 0.0;
for (size_t lutIndex = 0; lutIndex < settings->GetLutBlendStackSize(); lutIndex++)
{
LutBlendItem& lutBlendItem = settings->GetLutBlendItem(lutIndex);
if (lutIndex == 0)
{
type = lutBlendItem.m_shaperPreset;
customMinExposure = lutBlendItem.m_customMinExposure;
customMaxExposure = lutBlendItem.m_customMaxExposure;
}
else if (type != lutBlendItem.m_shaperPreset)
{
// Shapers are different
return AZStd::nullopt;
}
else if (type == ShaperPresetType::LinearCustomRange || type == ShaperPresetType::Log2CustomRange)
{
if (lutBlendItem.m_customMinExposure != customMinExposure ||
lutBlendItem.m_customMaxExposure != customMaxExposure)
{
// Shapers are same, but custom exposure for custom type is different.
return AZStd::nullopt;
}
}
}
// Only calculate shaper params when there's at least one lut blend.
if (settings->GetLutBlendStackSize() > 0)
{
return AcesDisplayMapperFeatureProcessor::GetShaperParameters(type, customMinExposure, customMaxExposure);
}
}
return AZStd::nullopt;
}
void BlendColorGradingLutsPass::CheckLutBlendSettings()
{
LookModificationSettings* settings = GetLookModificationSettings();
if (settings)
{
settings->PrepareLutBlending();
// Early out if the settings have not chanced
HashValue64 hash = settings->GetHash();
if (hash == m_lutBlendHash)
{
return;
}
m_lutBlendHash = hash;
m_needToUpdateLut = true;
// Calculate all the weights and LUT assets and check if there has been a change
// Only the top N LUTs will be blended where N = LookModificationSettings::MaxBlendLuts
// Weight 0 is used for the base color, and the other weights are for the LUTs in increasing priority
size_t numLuts = settings->GetLutBlendStackSize();
float intensity[LookModificationSettings::MaxBlendLuts];
float one_intensity[LookModificationSettings::MaxBlendLuts];
float over[LookModificationSettings::MaxBlendLuts];
float one_over[LookModificationSettings::MaxBlendLuts];
for (int curLutIndex = 0; curLutIndex < LookModificationSettings::MaxBlendLuts; curLutIndex++)
{
intensity[curLutIndex] = 0.f;
one_intensity[curLutIndex] = 1.f;
over[curLutIndex] = 0.f;
one_over[curLutIndex] = 1.f;
}
int current = 0;
for (size_t lutIndex = 0; lutIndex < numLuts; lutIndex++)
{
LutBlendItem& lutBlendItem = settings->GetLutBlendItem(lutIndex);
const auto assetId = lutBlendItem.m_assetId.GetId();
if (assetId.IsValid())
{
AcesDisplayMapperFeatureProcessor* dmfp = GetScene()->GetFeatureProcessor<AcesDisplayMapperFeatureProcessor>();
dmfp->GetLutFromAssetId(m_colorGradingLuts[current], assetId);
if (!m_colorGradingLuts[current].m_lutStreamingImage)
{
AZ_Warning("BlendColorGradingLutsPass", false, "Unable to load grading LUT from asset %s",
lutBlendItem.m_assetId.ToString<AZStd::string>().c_str());
// Skip this LUT
continue;
}
}
intensity[current] = lutBlendItem.m_intensity;
one_intensity[current] = 1.0f - lutBlendItem.m_intensity;
over[current] = lutBlendItem.m_overrideStrength;
one_over[current] = 1.0f - lutBlendItem.m_overrideStrength;
m_colorGradingShaperParams[current] = AcesDisplayMapperFeatureProcessor::GetShaperParameters(
lutBlendItem.m_shaperPreset,
lutBlendItem.m_customMinExposure,
lutBlendItem.m_customMaxExposure
);
++current;
if (current == LookModificationSettings::MaxBlendLuts)
{
break;
}
}
m_weights[0] = 0.f;
// Handle the case where there are no LUTs to be blended, and hence an identity LUT will be generated
if (current == 0)
{
m_weights[0] = 1.f;
// These weights would not be used in the shader in this case, but setting to zero anyways.
for (int lutIndex = 1; lutIndex < LookModificationSettings::MaxBlendLuts + 1; lutIndex++)
{
m_weights[lutIndex] = 0.f;
}
}
else
{
// Compute all the weights
// First compute the weight of the ungraded color value
for (int lutIndex = 0; lutIndex < current; lutIndex++)
{
float weight = one_intensity[lutIndex] * over[lutIndex];
for (int overrideLutIndex = lutIndex + 1; overrideLutIndex < LookModificationSettings::MaxBlendLuts; overrideLutIndex++)
{
weight *= one_over[overrideLutIndex];
}
m_weights[0] += weight;
}
// Then compute the weights for the LUTs
for (int weightIndex = 0; weightIndex < current; weightIndex++)
{
m_weights[weightIndex + 1] = intensity[weightIndex] * over[weightIndex];
for (int lutIndex = weightIndex + 1; lutIndex < LookModificationSettings::MaxBlendLuts; lutIndex++)
{
m_weights[weightIndex + 1] *= one_over[lutIndex];
}
}
}
// If the number of source LUTs have changed, the shader variant will need to be updated
if (m_numSourceLuts != current)
{
m_numSourceLuts = current;
m_needToUpdateShaderVariant = true;
}
}
}
LookModificationSettings* BlendColorGradingLutsPass::GetLookModificationSettings() const
{
AZ::RPI::Scene* scene = GetScene();
if (scene)
@@ -259,109 +422,12 @@ namespace AZ
LookModificationSettings* settings = postProcessSettings->GetLookModificationSettings();
if (settings)
{
settings->PrepareLutBlending();
// Early out if the settings have not chanced
HashValue64 hash = settings->GetHash();
if (hash == m_lutBlendHash)
{
return;
}
m_lutBlendHash = hash;
m_needToUpdateLut = true;
// Calculate all the weights and LUT assets and check if there has been a change
// Only the top N LUTs will be blended where N = LookModificationSettings::MaxBlendLuts
// Weight 0 is used for the base color, and the other weights are for the LUTs in increasing priority
size_t numLuts = settings->GetLutBlendStackSize();
float intensity[LookModificationSettings::MaxBlendLuts];
float one_intensity[LookModificationSettings::MaxBlendLuts];
float over[LookModificationSettings::MaxBlendLuts];
float one_over[LookModificationSettings::MaxBlendLuts];
for (int curLutIndex = 0; curLutIndex < LookModificationSettings::MaxBlendLuts; curLutIndex++)
{
intensity[curLutIndex] = 0.f;
one_intensity[curLutIndex] = 1.f;
over[curLutIndex] = 0.f;
one_over[curLutIndex] = 1.f;
}
int current = 0;
for (size_t lutIndex = 0; lutIndex < numLuts; lutIndex++)
{
LutBlendItem& lutBlendItem = settings->GetLutBlendItem(lutIndex);
auto assetId = lutBlendItem.m_assetId.GetId();
if (assetId.IsValid())
{
AcesDisplayMapperFeatureProcessor* dmfp = scene->GetFeatureProcessor<AcesDisplayMapperFeatureProcessor>();
dmfp->GetLutFromAssetId(m_colorGradingLuts[lutIndex], assetId);
if (!m_colorGradingLuts[lutIndex].m_lutStreamingImage)
{
AZ_Warning("BlendColorGradingLutsPass", false, "Unable to load grading LUT from asset %s", lutBlendItem.m_assetId.ToString<AZStd::string>().c_str());
// Skip this LUT
continue;
}
}
intensity[current] = lutBlendItem.m_intensity;
one_intensity[current] = 1.f - intensity[lutIndex];
over[current] = lutBlendItem.m_overrideStrength;
one_over[current] = 1.f - over[lutIndex];
m_colorGradingLutAssets[current] = lutBlendItem.m_assetId;
m_colorGradingShaperPresets[current] = lutBlendItem.m_shaperPreset;
m_colorGradingShaperParams[current] = AcesDisplayMapperFeatureProcessor::GetShaperParameters(m_colorGradingShaperPresets[lutIndex]);
current++;
if (current == LookModificationSettings::MaxBlendLuts)
{
break;
}
}
m_weights[0] = 0.f;
// Handle the case where there are no LUTs to be blended, and hence an identity LUT will be generated
if (current == 0)
{
m_weights[0] = 1.f;
// These weights would not be used in the shader in this case, but setting to zero anyways.
for (int lutIndex = 1; lutIndex < LookModificationSettings::MaxBlendLuts + 1; lutIndex++)
{
m_weights[lutIndex] = 0.f;
}
}
else
{
// Compute all the weights
// First compute the weight of the ungraded color value
for (int lutIndex = 0; lutIndex < LookModificationSettings::MaxBlendLuts; lutIndex++)
{
float weight = one_intensity[lutIndex] * over[lutIndex];
for (int overrideLutIndex = lutIndex + 1; overrideLutIndex < LookModificationSettings::MaxBlendLuts; overrideLutIndex++)
{
weight *= one_over[overrideLutIndex];
}
m_weights[0] += weight;
}
// Then compute the weights for the LUTs
for (int weightIndex = 0; weightIndex < LookModificationSettings::MaxBlendLuts; weightIndex++)
{
m_weights[weightIndex + 1] = intensity[weightIndex] * over[weightIndex];
for (int lutIndex = weightIndex + 1; lutIndex < LookModificationSettings::MaxBlendLuts; lutIndex++)
{
m_weights[weightIndex + 1] *= one_over[lutIndex];
}
}
}
// If the number of source LUTs have changed, the shader variant will need to be updated
if (m_numSourceLuts != current)
{
m_numSourceLuts = current;
m_needToUpdateShaderVariant = true;
}
return settings;
}
}
}
}
return nullptr;
}
} // namespace Render
} // namespace AZ
@@ -47,6 +47,7 @@ namespace AZ
static RPI::Ptr<BlendColorGradingLutsPass> Create(const RPI::PassDescriptor& descriptor);
void SetShaperParameters(const ShaperParams& shaperParams);
AZStd::optional<ShaperParams> GetCommonShaperParams() const;
private:
explicit BlendColorGradingLutsPass(const RPI::PassDescriptor& descriptor);
@@ -66,6 +67,7 @@ namespace AZ
void ReleaseLutImage();
void CheckLutBlendSettings();
LookModificationSettings* GetLookModificationSettings() const;
bool m_resourcesInitialized = false;
@@ -111,8 +113,6 @@ namespace AZ
AZStd::array<u32, 3> m_blendedLutDimensions;
float m_weights[LookModificationSettings::MaxBlendLuts + 1]; // The first index is reserved for the weight of the non color graded value
Data::Asset<RPI::AnyAsset> m_colorGradingLutAssets[LookModificationSettings::MaxBlendLuts];
ShaperPresetType m_colorGradingShaperPresets[LookModificationSettings::MaxBlendLuts];
Render::ShaperParams m_colorGradingShaperParams[LookModificationSettings::MaxBlendLuts];
Render::DisplayMapperAssetLut m_colorGradingLuts[LookModificationSettings::MaxBlendLuts];
@@ -9,11 +9,15 @@
#include <Atom/RPI.Public/RenderPipeline.h>
#include <Atom/RPI.Public/View.h>
#include <Atom/RPI.Public/Scene.h>
#include <Atom/RPI.Public/Pass/PassFilter.h>
#include <Atom/RPI.Public/Pass/PassSystem.h>
#include <Atom/RPI.Public/Shader/ShaderVariant.h>
#include <Atom/RHI/FrameScheduler.h>
#include <Atom/RHI/PipelineState.h>
#include <AzCore/Console/Console.h>
#include <PostProcessing/LookModificationCompositePass.h>
#include <PostProcess/PostProcessFeatureProcessor.h>
#include <PostProcess/ExposureControl/ExposureControlSettings.h>
@@ -22,6 +26,22 @@ namespace AZ
{
namespace Render
{
AZ_CVAR(uint8_t,
r_lutSampleQuality,
0,
[](const uint8_t& value)
{
auto passes = RPI::PassSystem::Get()->FindPasses(RPI::PassClassFilter<LookModificationCompositePass>());
for (auto* pass : passes)
{
LookModificationCompositePass* lookModPass = azrtti_cast<LookModificationCompositePass*>(pass);
lookModPass->SetSampleQuality(LookModificationCompositePass::SampleQuality(value));
}
},
ConsoleFunctorFlags::Null,
"This can be increased to deal with particularly tricky luts. Range (0-2). 0 (default) - Standard linear sampling. 1 - 7 tap b-spline sampling. 2 - 19 tap b-spline sampling."
);
RPI::Ptr<LookModificationCompositePass> LookModificationCompositePass::Create(const RPI::PassDescriptor& descriptor)
{
RPI::Ptr<LookModificationCompositePass> pass = aznew LookModificationCompositePass(descriptor);
@@ -30,8 +50,6 @@ namespace AZ
LookModificationCompositePass::LookModificationCompositePass(const RPI::PassDescriptor& descriptor)
: AZ::RPI::FullscreenTrianglePass(descriptor)
, m_exposureShaderVariantOptionName(ExposureShaderVariantOptionName)
, m_colorGradingShaderVariantOptionName(ColorGradingShaderVariantOptionName)
{
}
@@ -60,19 +78,38 @@ namespace AZ
{
AZ_Assert(m_shader != nullptr, "LookModificationCompositePass %s has a null shader when calling InitializeShaderVariant.", GetPathName().GetCStr());
AZStd::vector<AZ::Name> exposureVariationTypes = { AZ::Name("true"), AZ::Name("false") };
AZStd::vector<AZ::Name> colorGradingVariationTypes = { AZ::Name("true"), AZ::Name("false") };
struct OptionSettings
{
AZ::Name m_enableExposureControl;
AZ::Name m_enableColorGrading;
RPI::ShaderOptionValue m_lutSampleQuality;
auto exposureVariationTypeCount = exposureVariationTypes.size();
auto totalVariationCount = exposureVariationTypes.size() * colorGradingVariationTypes.size();
OptionSettings(const char* enableExposureControl, const char* enableColorGrading, SampleQuality sampleQuality)
: m_enableExposureControl(Name(enableExposureControl))
, m_enableColorGrading(Name(enableColorGrading))
, m_lutSampleQuality(RPI::ShaderOptionValue(sampleQuality))
{}
};
AZStd::vector<OptionSettings> options =
{
{ "false", "false", SampleQuality::Linear },
{ "true", "false", SampleQuality::Linear },
{ "false", "true", SampleQuality::Linear },
{ "false", "true", SampleQuality::BSpline7Tap },
{ "false", "true", SampleQuality::BSpline19Tap },
{ "true", "true", SampleQuality::Linear },
{ "true", "true", SampleQuality::BSpline7Tap },
{ "true", "true", SampleQuality::BSpline19Tap },
};
// Caching all pipeline state for each shader variation for performance reason.
for (auto shaderVariantIndex = 0; shaderVariantIndex < totalVariationCount; ++shaderVariantIndex)
for (auto shaderVariantIndex = 0; shaderVariantIndex < options.size(); ++shaderVariantIndex)
{
auto shaderOption = m_shader->CreateShaderOptionGroup();
shaderOption.SetValue(m_exposureShaderVariantOptionName, exposureVariationTypes[shaderVariantIndex % exposureVariationTypeCount]);
shaderOption.SetValue(m_colorGradingShaderVariantOptionName, colorGradingVariationTypes[shaderVariantIndex / exposureVariationTypeCount]);
shaderOption.SetValue(m_exposureShaderVariantOptionName, options.at(shaderVariantIndex).m_enableExposureControl);
shaderOption.SetValue(m_colorGradingShaderVariantOptionName, options.at(shaderVariantIndex).m_enableColorGrading);
shaderOption.SetValue(m_lutSampleQualityShaderVariantOptionName, options.at(shaderVariantIndex).m_lutSampleQuality);
PreloadShaderVariant(m_shader, shaderOption, GetRenderAttachmentConfiguration(), GetMultisampleState());
}
@@ -173,9 +210,9 @@ namespace AZ
{
m_shaderResourceGroup->SetImageView(m_shaderColorGradingLutImageIndex, m_blendedColorGradingLut.m_lutImageView.get());
m_shaderResourceGroup->SetConstant(m_shaderColorGradingShaperTypeIndex, m_colorGradingShaperParams.type);
m_shaderResourceGroup->SetConstant(m_shaderColorGradingShaperBiasIndex, m_colorGradingShaperParams.bias);
m_shaderResourceGroup->SetConstant(m_shaderColorGradingShaperScaleIndex, m_colorGradingShaperParams.scale);
m_shaderResourceGroup->SetConstant(m_shaderColorGradingShaperTypeIndex, m_colorGradingShaperParams.m_type);
m_shaderResourceGroup->SetConstant(m_shaderColorGradingShaperBiasIndex, m_colorGradingShaperParams.m_bias);
m_shaderResourceGroup->SetConstant(m_shaderColorGradingShaperScaleIndex, m_colorGradingShaperParams.m_scale);
}
}
@@ -192,7 +229,8 @@ namespace AZ
// Decide which shader to use.
shaderOption.SetValue(m_exposureShaderVariantOptionName, m_exposureControlEnabled ? AZ::Name("true") : AZ::Name("false"));
shaderOption.SetValue(m_colorGradingShaderVariantOptionName, m_colorGradingLutEnabled ? AZ::Name("true") : AZ::Name("false"));
shaderOption.SetValue(m_lutSampleQualityShaderVariantOptionName, RPI::ShaderOptionValue(m_sampleQuality));
UpdateShaderVariant(shaderOption);
m_needToUpdateShaderVariant = false;
@@ -218,5 +256,12 @@ namespace AZ
{
m_colorGradingShaperParams = shaperParams;
}
void LookModificationCompositePass::SetSampleQuality(SampleQuality sampleQuality)
{
m_sampleQuality = sampleQuality;
m_needToUpdateShaderVariant = true;
}
} // namespace Render
} // namespace AZ
@@ -30,8 +30,6 @@ namespace AZ
namespace Render
{
static const char* const LookModificationTransformPassTemplateName{ "LookModificationTransformTemplate" };
static const char* const ExposureShaderVariantOptionName{ "o_enableExposureControlFeature" };
static const char* const ColorGradingShaderVariantOptionName{ "o_enableColorGradingLut" };
/**
* The look modification composite pass. If color grading LUTs are enabled, this pass will apply the blended LUT.
@@ -43,6 +41,14 @@ namespace AZ
public:
AZ_RTTI(LookModificationCompositePass, "{D7DF3E8A-B642-4D51-ABC2-ADB2B60FCE1D}", AZ::RPI::FullscreenTrianglePass);
AZ_CLASS_ALLOCATOR(LookModificationCompositePass, SystemAllocator, 0);
enum class SampleQuality : uint8_t
{
Linear = 0,
BSpline7Tap = 1,
BSpline19Tap = 2,
};
virtual ~LookModificationCompositePass() = default;
//! Creates a LookModificationPass
@@ -54,6 +60,8 @@ namespace AZ
//! Set shaper parameters
void SetShaperParameters(const ShaperParams& shaperParams);
void SetSampleQuality(SampleQuality sampleQuality);
protected:
LookModificationCompositePass(const RPI::PassDescriptor& descriptor);
@@ -76,11 +84,15 @@ namespace AZ
bool m_exposureControlEnabled = false;
bool m_colorGradingLutEnabled = false;
SampleQuality m_sampleQuality = SampleQuality::Linear;
Render::DisplayMapperLut m_blendedColorGradingLut;
Render::ShaperParams m_colorGradingShaperParams;
const AZ::Name m_exposureShaderVariantOptionName;
const AZ::Name m_colorGradingShaderVariantOptionName;
const AZ::Name m_exposureShaderVariantOptionName{ "o_enableExposureControlFeature" };
const AZ::Name m_colorGradingShaderVariantOptionName{ "o_enableColorGradingLut" };
const AZ::Name m_lutSampleQualityShaderVariantOptionName{ "o_lutSampleQuality" };
bool m_needToUpdateShaderVariant = true;
RHI::ShaderInputNameIndex m_shaderColorGradingLutImageIndex = "m_gradingLut";
@@ -47,29 +47,32 @@ namespace AZ
swapChainFormat = m_swapChainAttachmentBinding->m_attachment->GetTransientImageDescriptor().m_imageDescriptor.m_format;
}
if (m_displayBufferFormat != swapChainFormat)
// Update the children passes
RPI::Ptr<BlendColorGradingLutsPass> blendPass = FindChildPass<BlendColorGradingLutsPass>();
if (blendPass)
{
m_displayBufferFormat = swapChainFormat;
m_outputDeviceTransformType = AcesDisplayMapperFeatureProcessor::GetOutputDeviceTransformType(m_displayBufferFormat);
m_shaperParams = GetAcesShaperParameters(m_outputDeviceTransformType);
// Update the children passes
for (const AZ::RPI::Ptr<Pass>& child : m_children)
auto commonShaperParams = blendPass->GetCommonShaperParams();
if (commonShaperParams)
{
BlendColorGradingLutsPass* blendPass = azrtti_cast<BlendColorGradingLutsPass*>(child.get());
if (blendPass)
{
blendPass->SetShaperParameters(m_shaperParams);
continue;
}
LookModificationCompositePass* compositePass = azrtti_cast<LookModificationCompositePass*>(child.get());
if (compositePass)
{
compositePass->SetShaperParameters(m_shaperParams);
continue;
}
m_shaperParams = *commonShaperParams;
}
else
{
// Mix of shapers used, so shape them based on the output transform type.
m_displayBufferFormat = swapChainFormat;
m_outputDeviceTransformType = AcesDisplayMapperFeatureProcessor::GetOutputDeviceTransformType(m_displayBufferFormat);
m_shaperParams = GetAcesShaperParameters(m_outputDeviceTransformType);
}
blendPass->SetShaperParameters(m_shaperParams);
RPI::Ptr<LookModificationCompositePass> compositePass = FindChildPass<LookModificationCompositePass>();
if (compositePass)
{
compositePass->SetShaperParameters(m_shaperParams);
}
}
ParentPass::FrameBeginInternal(params);
}
} // namespace Render
@@ -64,6 +64,9 @@ namespace AZ
//! Find a child pass with a matching name and returns it. Return nullptr if none found.
Ptr<Pass> FindChildPass(const Name& passName) const;
template<typename PassType>
Ptr<PassType> FindChildPass() const;
//! Searches the tree for the first pass that has same pass name (Depth-first search). Return nullptr if none found.
Ptr<Pass> FindPassByNameRecursive(const Name& passName) const;
@@ -132,5 +135,20 @@ namespace AZ
// Generates child passes from source PassTemplate
void CreatePassesFromTemplate();
};
template<typename PassType>
inline Ptr<PassType> ParentPass::FindChildPass() const
{
for (const Ptr<Pass>& child : m_children)
{
PassType* pass = azrtti_cast<PassType*>(child.get());
if (pass)
{
return pass;
}
}
return {};
}
} // namespace RPI
} // namespace AZ
@@ -39,6 +39,11 @@ namespace AZ
void CopySettingsTo(LookModificationSettingsInterface* settings);
bool ArePropertiesReadOnly() const { return !m_enabled; }
bool IsUsingCustomShaper() const {
return m_shaperPresetType == ShaperPresetType::LinearCustomRange
|| m_shaperPresetType == ShaperPresetType::Log2CustomRange;
}
};
}
}
@@ -48,33 +48,44 @@ namespace AZ
&LookModificationComponentConfig::m_enabled,
"Enable look modification",
"Enable look modification.")
->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
->DataElement(AZ::Edit::UIHandlers::Default, &LookModificationComponentConfig::m_colorGradingLut, "Color Grading LUT", "Color grading LUT")
->ClassElement(Edit::ClassElements::EditorData, "")
->DataElement(Edit::UIHandlers::ComboBox,
&LookModificationComponentConfig::m_shaperPresetType,
"Shaper Type",
"Shaper Type.")
->EnumAttribute(ShaperPresetType::None, "None")
->EnumAttribute(ShaperPresetType::Log2_48_nits, "Log2_48_nits")
->EnumAttribute(ShaperPresetType::Log2_1000_nits, "Log2_1000_nits")
->EnumAttribute(ShaperPresetType::Log2_2000_nits, "Log2_2000_nits")
->EnumAttribute(ShaperPresetType::Log2_4000_nits, "Log2_4000_nits")
->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
&LookModificationComponentConfig::m_shaperPresetType, "Shaper Type", "Shaper Type.")
->EnumAttribute(ShaperPresetType::None, "None")
->EnumAttribute(ShaperPresetType::LinearCustomRange, "Linear Custom Range")
->EnumAttribute(ShaperPresetType::Log2_48Nits, "Log2 48 nits")
->EnumAttribute(ShaperPresetType::Log2_1000Nits, "Log2 1000 nits")
->EnumAttribute(ShaperPresetType::Log2_2000Nits, "Log2 2000 nits")
->EnumAttribute(ShaperPresetType::Log2_4000Nits, "Log2 4000 nits")
->EnumAttribute(ShaperPresetType::Log2CustomRange, "Log2 Custom Range")
->EnumAttribute(ShaperPresetType::PqSmpteSt2084, "PQ (SMPTE ST 2084)")
->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::EntireTree)
->DataElement(AZ::Edit::UIHandlers::Slider, &LookModificationComponentConfig::m_customMinExposure, "Minimum Exposure", "The minimum exposure this LUT supports. Values smaller than this will be clamped to 0.")
->Attribute(AZ::Edit::Attributes::Min, -50.0f)
->Attribute(AZ::Edit::Attributes::Max, 0.0f)
->Attribute(AZ::Edit::Attributes::SoftMin, -20.0f)
->Attribute(AZ::Edit::Attributes::SoftMax, 0.0f)
->Attribute(Edit::Attributes::Visibility, &LookModificationComponentConfig::IsUsingCustomShaper)
->Attribute(AZ::Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
->DataElement(AZ::Edit::UIHandlers::Slider, &LookModificationComponentConfig::m_customMaxExposure, "Maximum Exposure", "The maximum exposure this LUT supports. Values larger than this will be clamped.")
->Attribute(AZ::Edit::Attributes::Min, 0.0f)
->Attribute(AZ::Edit::Attributes::Max, 50.0f)
->Attribute(AZ::Edit::Attributes::SoftMin, 0.0f)
->Attribute(AZ::Edit::Attributes::SoftMax, 20.0f)
->Attribute(Edit::Attributes::Visibility, &LookModificationComponentConfig::IsUsingCustomShaper)
->Attribute(AZ::Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
->DataElement(AZ::Edit::UIHandlers::Slider, &LookModificationComponentConfig::m_colorGradingLutIntensity, "LUT Intensity", "Blend intensity of this LUT.")
->Attribute(AZ::Edit::Attributes::Min, 0.0f)
->Attribute(AZ::Edit::Attributes::Max, 1.0f)
->Attribute(AZ::Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
->Attribute(Edit::Attributes::ReadOnly, &LookModificationComponentConfig::ArePropertiesReadOnly)
->Attribute(AZ::Edit::Attributes::Min, 0.0f)
->Attribute(AZ::Edit::Attributes::Max, 1.0f)
->Attribute(AZ::Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
->Attribute(Edit::Attributes::ReadOnly, &LookModificationComponentConfig::ArePropertiesReadOnly)
->DataElement(AZ::Edit::UIHandlers::Slider, &LookModificationComponentConfig::m_colorGradingLutOverride, "LUT Override", "Blend intensity of this LUT.")
->Attribute(AZ::Edit::Attributes::Min, 0.0f)
->Attribute(AZ::Edit::Attributes::Max, 1.0f)
->Attribute(AZ::Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
->Attribute(Edit::Attributes::ReadOnly, &LookModificationComponentConfig::ArePropertiesReadOnly)
->Attribute(AZ::Edit::Attributes::Min, 0.0f)
->Attribute(AZ::Edit::Attributes::Max, 1.0f)
->Attribute(AZ::Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
->Attribute(Edit::Attributes::ReadOnly, &LookModificationComponentConfig::ArePropertiesReadOnly)
// Overrides
->ClassElement(AZ::Edit::ClassElements::Group, "Overrides")