TECHARTNOMAD | TECHARTFLOWIO.COM

UNITY3D

How to port UE4 ACES to URP

jplee 2023. 9. 1. 00:11

 

이 과정에 사용된 Unity 버전은 2021.1.18로, URP 버전 11을 기반으로 작성되었습니다.

다만 아래의 내용을 참조 하여 거의 모든 URP 버전에 통합 할 수 있을 것입니다.

 

UE4에서 사용되는 ACES 톤 매핑을 유니티 엔진 URP로 포팅하려고 합니다.
이 과정을 통해 향후 다양한 톤 매핑 로직을 유연하게 추가할 수 있습니다.

 

위와 동일한 형식으로 추가합니다.

 

Adding tone mapping mode.

Tonemapping.cs

public enum TonemappingMode
    {
        None,
        Neutral, // Neutral tonemapper
        ACES,    // ACES Filmic reference tonemapper (custom approximation)
        Custom,
        /////////////////UE4_ACES_BEGIN///////////////
        UE4_ACES,
       /////////////////UE4_ACES_END/////////////////
    }

UE4 ACES를 추가했습니다.

One more mode has been added.

이제 몇 가지 옵션을 추가해 봅시다.

톤 매핑 클래스에 변수를 추가하겠습니다.

 

Tonemapping.cs

public sealed class Tonemapping : VolumeComponent, IPostProcessComponent
    {
        [Tooltip("Select a tonemapping algorithm to use for the color grading process.")]
        public TonemappingModeParameter mode = new TonemappingModeParameter(TonemappingMode.None);
        
        /////////////////UE4_ACES_BEGIN/////////////////
        //create floatParameter
        /// This is only used when <see cref="Tonemapper.UE4_ACES"/> is active.
        [Tooltip("Film_Slope")]
        public ClampedFloatParameter slope = new ClampedFloatParameter(0.88f, 0f, 1f);
        /// This is only used when <see cref="Tonemapper.UE4_ACES"/> is active.
        [Tooltip("Film_Toe")]
        public ClampedFloatParameter toe = new ClampedFloatParameter(0.55f, 0.0f, 1.0f);
        /// This is only used when <see cref="Tonemapper.UE4_ACES"/> is active.
        [Tooltip("Film_Shoulder")]
        public ClampedFloatParameter shoulder = new ClampedFloatParameter(0.26f, 0.0f, 1.0f);
        /// This is only used when <see cref="Tonemapper.UE4_ACES"/> is active.
        [Tooltip("Film_BlackClip")]
        public ClampedFloatParameter blackClip = new ClampedFloatParameter(0.0f, 0.0f, 1.0f);
        /// This is only used when <see cref="Tonemapper.UE4_ACES"/> is active.
        [Tooltip("Film_WhiteClip")]
        public ClampedFloatParameter whiteClip = new ClampedFloatParameter(0.04f, 0.0f, 1.0f);
        /////////////////UE4_ACES_END/////////////////        

        public bool IsActive() => mode.value != TonemappingMode.None;

        public bool IsTileCompatible() => true;
    }

이제 추가된 변수에서 얻은 값을 편집기에 전달해야 합니다.

먼저 에디터에 SerializedDataParameter 5개를 추가해 보겠습니다.

 

TonemappingEditor.cs

/////////////////UE4_ACES_BEGIN/////////////////
SerializedDataParameter m_Slope;
SerializedDataParameter m_Toe;
SerializedDataParameter m_Shoulder;
SerializedDataParameter m_BlackClip;
SerializedDataParameter m_WhiteClip;
/////////////////UE4_ACES_END/////////////////

TonemappingEditor.cs

public override void OnEnable()
{
    var o = new PropertyFetcher<Tonemapping>(serializedObject);

    m_Mode = Unpack(o.Find(x => x.mode));
    /////////////////UE4_ACES_BEGIN/////////////////
    m_Slope = Unpack(o.Find(x => x.slope));
    m_Toe = Unpack(o.Find(x => x.toe));
    m_Shoulder = Unpack(o.Find(x => x.shoulder));
    m_BlackClip = Unpack(o.Find(x => x.blackClip));
    m_WhiteClip = Unpack(o.Find(x => x.whiteClip));
    /////////////////UE4_ACES_END///////////////////

}

재설정 버튼 UI 추가

TonemappingEditor.cs

OnInspectorGUI() 함수에서 GUILayout.Button() API를 사용하여 재설정 버튼을 추가합니다.

Unity - Scripting API: GUILayout (unity3d.com)

 

Unity - Scripting API: GUILayout

Success! Thank you for helping us improve the quality of Unity Documentation. Although we cannot accept all submissions, we do read each suggested change from our users and will make updates where applicable. Close

docs.unity3d.com

public override void OnInspectorGUI()
{
    PropertyField(m_Mode);
    /////////////////UE4_ACES_BEGIN/////////////////
    if ( m_Mode.value.intValue == (int)TonemappingMode.UE4_ACES)
    {
        UnityEngine.GUILayout.BeginVertical("box");
        UnityEngine.GUILayout.BeginHorizontal();

        PropertyField(m_Slope);
        if (UnityEngine.GUILayout.Button("Reset"))
        {
            m_Slope.value.floatValue = 0.88f;
        }
        UnityEngine.GUILayout.EndHorizontal();

        UnityEngine.GUILayout.BeginHorizontal();
        PropertyField(m_Toe);
        if (UnityEngine.GUILayout.Button("Reset"))
        {
            m_Toe.value.floatValue = 0.55f;
        }
        UnityEngine.GUILayout.EndHorizontal();

        UnityEngine.GUILayout.BeginHorizontal();
        PropertyField(m_Shoulder);
        if (UnityEngine.GUILayout.Button("Reset"))
        {
            m_Shoulder.value.floatValue = 0.26f;
        }
        UnityEngine.GUILayout.EndHorizontal();

        UnityEngine.GUILayout.BeginHorizontal();
        PropertyField(m_BlackClip);
        if (UnityEngine.GUILayout.Button("Reset"))
        {
            m_BlackClip.value.floatValue = 0.0f;
        }
        UnityEngine.GUILayout.EndHorizontal();

        UnityEngine.GUILayout.BeginHorizontal();
        PropertyField(m_WhiteClip);
        if (UnityEngine.GUILayout.Button("Reset"))
        {
            m_WhiteClip.value.floatValue = 0.04f;
        }
        UnityEngine.GUILayout.EndHorizontal();
        UnityEngine.GUILayout.EndVertical();

    }
    /////////////////UE4_ACES_END////////////////////

    // Display a warning if the user is trying to use a tonemap while rendering in LDR
    var asset = UniversalRenderPipeline.asset;
    if (asset != null && !asset.supportsHDR)
    {
        EditorGUILayout.HelpBox("Tonemapping should only be used when working in HDR.", MessageType.Warning);
        return;
    }
}

Using 을 사용하여 UnityEngine 네임스페이스를 임포트할 수도 있습니다.

재설정 버튼을 클릭하면 직렬화된 각 프로퍼티가 기본값인 초기값으로 다시 가져옵니다. 

좋습니다. 기능은 작동하지 않지만 UI는 의도한 대로 잘 갖춰져 있습니다.  

 

Properties and shader integration.

UE4 엔진의 ACES 구조를 참조하여 작성한 셰이더 코드를 추가해 보겠습니다.

Packages/com.unity.render-pipelines.universal@11.0.0/ShaderLibrary/UE4_ACES.hlsl

UE4_ACES.hlsl 형식으로 위 경로에 추가합니다.

#ifndef UNITY_UE4ACES_INCLUDED
#define UNITY_UE4ACES_INCLUDED

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/ACES.hlsl"

float FilmSlope;// = 0.91;
float FilmToe;// = 0.53;
float FilmShoulder;// = 0.23;
float FilmBlackClip;// = 0;
float FilmWhiteClip;// = 0.035;

float3 UE4ACES(float3 aces)
{
	// "Glow" module constants
	const float RRT_GLOW_GAIN = 0.05;
	const float RRT_GLOW_MID = 0.08;

	float saturation = rgb_2_saturation(aces);
	float ycIn = rgb_2_yc(aces);
	float s = sigmoid_shaper((saturation - 0.4) / 0.2);
	float addedGlow = 1.0 + glow_fwd(ycIn, RRT_GLOW_GAIN * s, RRT_GLOW_MID);
	aces *= addedGlow;

	const float RRT_RED_SCALE = 0.82;
	const float RRT_RED_PIVOT = 0.03;
	const float RRT_RED_HUE = 0.0;
	const float RRT_RED_WIDTH = 135.0;

	// --- Red modifier --- //
	float hue = rgb_2_hue(aces);
	float centeredHue = center_hue(hue, RRT_RED_HUE);
	float hueWeight;
	{
		hueWeight = smoothstep(0.0, 1.0, 1.0 - abs(2.0 * centeredHue / RRT_RED_WIDTH));
		hueWeight *= hueWeight;
	}
	//float hueWeight = Square( smoothstep(0.0, 1.0, 1.0 - abs(2.0 * centeredHue / RRT_RED_WIDTH)) );

	aces.r += hueWeight * saturation * (RRT_RED_PIVOT - aces.r) * (1.0 - RRT_RED_SCALE);

	// Use ACEScg primaries as working space
	float3 acescg = max(0.0, ACES_to_ACEScg(aces));

	// Pre desaturate
	acescg = lerp(dot(acescg, AP1_RGB2Y).xxx, acescg, 0.96);

	const half ToeScale = 1 + FilmBlackClip - FilmToe;
	const half ShoulderScale = 1 + FilmWhiteClip - FilmShoulder;

	const float InMatch = 0.18;
	const float OutMatch = 0.18;

	float ToeMatch;
	if (FilmToe > 0.8)
	{
		// 0.18 will be on straight segment
		ToeMatch = (1 - FilmToe - OutMatch) / FilmSlope + log10(InMatch);
	}
	else
	{
		// 0.18 will be on toe segment

		// Solve for ToeMatch such that input of InMatch gives output of OutMatch.
		const float bt = (OutMatch + FilmBlackClip) / ToeScale - 1;
		ToeMatch = log10(InMatch) - 0.5 * log((1 + bt) / (1 - bt)) * (ToeScale / FilmSlope);
	}

	float StraightMatch = (1 - FilmToe) / FilmSlope - ToeMatch;
	float ShoulderMatch = FilmShoulder / FilmSlope - StraightMatch;

	half3 LogColor = log10(acescg);
	half3 StraightColor = FilmSlope * (LogColor + StraightMatch);

	half3 ToeColor = (-FilmBlackClip) + (2 * ToeScale) / (1 + exp((-2 * FilmSlope / ToeScale) * (LogColor - ToeMatch)));
	half3 ShoulderColor = (1 + FilmWhiteClip) - (2 * ShoulderScale) / (1 + exp((2 * FilmSlope / ShoulderScale) * (LogColor - ShoulderMatch)));

	ToeColor = LogColor < ToeMatch ? ToeColor : StraightColor;
	ShoulderColor = LogColor > ShoulderMatch ? ShoulderColor : StraightColor;

	half3 t = saturate((LogColor - ToeMatch) / (ShoulderMatch - ToeMatch));
	t = ShoulderMatch < ToeMatch ? 1 - t : t;
	t = (3 - 2 * t)*t*t;
	half3 linearCV = lerp(ToeColor, ShoulderColor, t);

	// Post desaturate
	linearCV = lerp(dot(float3(linearCV), AP1_RGB2Y), linearCV, 0.93);

	// Returning positive AP1 values
	//return max(0, linearCV);

	// Convert to display primary encoding
	// Rendering space RGB to XYZ
	float3 XYZ = mul(AP1_2_XYZ_MAT, linearCV);

	// Apply CAT from ACES white point to assumed observer adapted white point
	XYZ = mul(D60_2_D65_CAT, XYZ);

	// CIE XYZ to display primaries
	linearCV = mul(XYZ_2_REC709_MAT, XYZ);

	linearCV = saturate(linearCV); //Protection to make negative return out.

	return linearCV;

}
/////////////////SWS_UE4_ACES_END/////////////////

#endif

UE4의 unity3D용 ACES 코드

구현할 때 종종 씬 뷰에서 확인하는데, 채도는 씬 뷰에서 내부적으로 작동하는 것 같습니다.

linearCV = saturate(linearCV); 추가하지 않으면 게임 뷰에 블랙 클립에 의한 음수 값이 반환됩니다.

 

UE4 ACES 톤 매핑은 기본적으로 ODT_Rec709_100nits_dim을 사용합니다.

유니티에서 사용되는 ACES의 ODT_Rec709_100nits_dim( ) 함수만 살펴봅시다.

//Packages/com.unity.render-pipelines.core@11.0.0/ShaderLibrary/ACES.hlsl
// CIE XYZ to display primaries
linearCV = mul(XYZ_2_REC709_MAT, XYZ);

// Handle out-of-gamut values
// Clip values < 0 or > 1 (i.e. projecting outside the display primaries)
linearCV = saturate(linearCV);

ODT_Rec709_100nits_dim

 

ACES 코드 브리핑은 일단 생략하겠습니다. 아주 길고 긴 설명이 필요하니 다음 기회에...

하지만 관심이 있으시다면 아래 링크를 참조하시기 바랍니다.

ampas/aces-dev: AMPAS Academy Color Encoding System Developer Resources (github.com)

 

GitHub - ampas/aces-dev: AMPAS Academy Color Encoding System Developer Resources

AMPAS Academy Color Encoding System Developer Resources - GitHub - ampas/aces-dev: AMPAS Academy Color Encoding System Developer Resources

github.com

개인적으로 이 ACES를 완전히 이해하려면 거의 반년 동안 관련 주제를 다양하게 살펴봐야 할 것 같습니다.

 

Added Shader Keyword.

Packages/com.unity.render-pipelines.universal@11.0.0/Runtime/UniversalRenderPipelineCore.cs

public static class ShaderKeywordStrings
{
    ...
    public static readonly string TonemapACES = "_TONEMAP_ACES";
    public static readonly string TonemapNeutral = "_TONEMAP_NEUTRAL";
    /////////////////UE4_ACES_END/////////////////
    public static readonly string TonemapUE4ACES = "_TONEMAP_UE4ACES";
    /////////////////UE4_ACES_END/////////////////
...
}

public static readonly string TonemapUE4ACES = "_TONEMAP_UE4ACES"; 를 추가합니다.

 

Packages/com.unity.render-pipelines.universal@11.0.0/Runtime/Passes/PostProcessPass.cs

void SetupColorGrading(CommandBuffer cmd, ref RenderingData renderingData, Material material)
{
    ref var postProcessingData = ref renderingData.postProcessingData;
    bool hdr = postProcessingData.gradingMode == ColorGradingMode.HighDynamicRange;
    int lutHeight = postProcessingData.lutSize;
    int lutWidth = lutHeight * lutHeight;

    // Source material setup
    float postExposureLinear = Mathf.Pow(2f, m_ColorAdjustments.postExposure.value);
    cmd.SetGlobalTexture(ShaderConstants._InternalLut, m_InternalLut.Identifier());
    material.SetVector(ShaderConstants._Lut_Params, new Vector4(1f / lutWidth, 1f / lutHeight, lutHeight - 1f, postExposureLinear));
    material.SetTexture(ShaderConstants._UserLut, m_ColorLookup.texture.value);
    material.SetVector(ShaderConstants._UserLut_Params, !m_ColorLookup.IsActive()
        ? Vector4.zero
        : new Vector4(1f / m_ColorLookup.texture.value.width,
            1f / m_ColorLookup.texture.value.height,
            m_ColorLookup.texture.value.height - 1f,
            m_ColorLookup.contribution.value)
    );

    if (hdr)
    {
        material.EnableKeyword(ShaderKeywordStrings.HDRGrading);
    }
    else
    {
        switch (m_Tonemapping.mode.value)
        {
            case TonemappingMode.Neutral: material.EnableKeyword(ShaderKeywordStrings.TonemapNeutral); break;
            case TonemappingMode.ACES: material.EnableKeyword(ShaderKeywordStrings.TonemapACES); break;
            /////////////////UE4_ACES_BEGIN/////////////////
            case TonemappingMode.UE4_ACES: material.EnableKeyword(ShaderKeywordStrings.TonemapUE4ACES); break;
            default: break; // None
        }
        //////LDR Bind UI
        material.SetFloat("FilmSlope", (float)m_Tonemapping.slope);
        material.SetFloat("FilmToe", (float)m_Tonemapping.toe);
        material.SetFloat("FilmShoulder", (float)m_Tonemapping.shoulder);
        material.SetFloat("FilmBlackClip", (float)m_Tonemapping.blackClip);
        material.SetFloat("FilmWhiteClip", (float)m_Tonemapping.whiteClip);
    }
}
case TonemappingMode.UE4_ACES: material.EnableKeyword(ShaderKeywordStrings.TonemapUE4ACES); break;

셰이더에 전송할 인수를 설정합니다.

material.SetFloat("FilmSlope", (float)m_Tonemapping.slope);
 material.SetFloat("FilmToe", (float)m_Tonemapping.toe);
 material.SetFloat("FilmShoulder", (float)m_Tonemapping.shoulder);
 material.SetFloat("FilmBlackClip", (float)m_Tonemapping.blackClip);
 material.SetFloat("FilmWhiteClip", (float)m_Tonemapping.whiteClip);

 

Tone mapping applied.

Packages/src/com.unity.render-pipelines.universal@7.2.1/Shaders/PostProcessing/Common.hlsl

#ifndef UNIVERSAL_POSTPROCESSING_COMMON_INCLUDED
#define UNIVERSAL_POSTPROCESSING_COMMON_INCLUDED

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"
#include "Packages/com.unity.render-pipelines.universal/Shaders/Utils/Fullscreen.hlsl"
/////////////////UE4_ACES_BEGIN///////////////
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/UE4_ACES.hlsl"
/////////////////UE4_ACES_END/////////////////

// ----------------------------------------------------------------------------------
// Render fullscreen mesh by using a matrix set directly by the pipeline instead of
// relying on the matrix set by the C++ engine to avoid issues with XR
half3 ApplyTonemap(half3 input)
{
#if _TONEMAP_ACES
    float3 aces = unity_to_ACES(input);
    input = AcesTonemap(aces);
#elif _TONEMAP_NEUTRAL
    input = NeutralTonemap(input);
    ///////////////////UE4_ACES_BEGIN/////////////////
#elif _TONEMAP_UE4ACES
    float3 aces = unity_to_ACES(input);
    input = UE4ACES(aces);
    ///////////////////UE4_ACES_END/////////////////
#endif

    return saturate(input);
}

 

Added multi-compilation keyword to UberPost.

UberPost에 멀티컴파일 키워드를 추가했습니다.

Assets/BundleRes/Shader/PostEffect/UberPost.shader

Shader "Hidden/Universal Render Pipeline/UberPost"
{
Properties
{
    [HideInInspector] _StencilRef("_StencilRef", Int) = 0
}
HLSLINCLUDE
    
#pragma multi_compile_local _ _DISTORTION
#pragma multi_compile_local _ _CHROMATIC_ABERRATION
#pragma multi_compile_local _ _BLOOM_LQ _BLOOM_HQ _BLOOM_LQ_DIRT _BLOOM_HQ_DIRT
#pragma multi_compile_local _ _HDR_GRADING _TONEMAP_ACES _TONEMAP_NEUTRAL _TONEMAP_UE4ACES /////////////////UE4_ACES_BEGIN/////////////////
#pragma multi_compile_local _ _FILM_GRAIN
#pragma multi_compile_local _ _DITHERING
#pragma multi_compile_local _ _LINEAR_TO_SRGB_CONVERSION

 

Add operation to lut builder.

Assets/BundleRes/Shader/PostEffect/LutBuilderHdr.shader

// Note: when the ACES tonemapper is selected the grading steps will be done using ACES spaces
float3 ColorGrade(float3 colorLutSpace)
{
    // Switch back to linear
    float3 colorLinear = LogCToLinear(colorLutSpace);

    // White balance in LMS space
    float3 colorLMS = LinearToLMS(colorLinear);
    colorLMS *= _ColorBalance.xyz;
    colorLinear = LMSToLinear(colorLMS);

    // Do contrast in log after white balance
    /////////////////UE4_ACES_BEGIN///////////////
    #if _TONEMAP_ACES || _TONEMAP_UE4ACES
    float3 colorLog = ACES_to_ACEScc(unity_to_ACES(colorLinear));
    /////////////////UE4_ACES_END///////////////
    #else
    float3 colorLog = LinearToLogC(colorLinear);
    #endif

    colorLog = (colorLog - ACEScc_MIDGRAY) * _HueSatCon.z + ACEScc_MIDGRAY;

    /////////////////UE4_ACES_BEGIN///////////////
    #if _TONEMAP_ACES || _TONEMAP_UE4ACES
    colorLinear = ACES_to_ACEScg(ACEScc_to_ACES(colorLog));
    /////////////////UE4_ACES_END///////////////
    #else
    colorLinear = LogCToLinear(colorLog);
    #endif

    // Color filter is just an unclipped multiplier
    colorLinear *= _ColorFilter.xyz;

    // Do NOT feed negative values to the following color ops
    colorLinear = max(0.0, colorLinear);
...
)

ColorGrade()에 _TONEMAP_UE4ACES 전처리 키워드를 추가합니다.

float3 Tonemap(float3 colorLinear)
{
    #if _TONEMAP_NEUTRAL
    {
        colorLinear = NeutralTonemap(colorLinear);
    }
    #elif _TONEMAP_ACES
    {
        // Note: input is actually ACEScg (AP1 w/ linear encoding)
        float3 aces = ACEScg_to_ACES(colorLinear);
        colorLinear = AcesTonemap(aces);
    }
    /////////////////UE4_ACES_BEGIN/////////////////
    #elif _TONEMAP_UE4ACES
    {
        float3 aces = ACEScg_to_ACES(colorLinear);
        colorLinear = UE4ACES(aces);
    }
		/////////////////UE4_ACES_END/////////////////
    #endif

    return colorLinear;
}

Packages/com.unity.render-pipelines.universal@11.0.0/Runtime/Passes/ColorGradingLutPass.cs

public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
    var cmd = CommandBufferPool.Get();
    using (new ProfilingScope(cmd, ProfilingSampler.Get(URPProfileId.ColorGradingLUT)))
    {
        ...

        // Secondary curves
        material.SetTexture(ShaderConstants._CurveHueVsHue, curves.hueVsHue.value.GetTexture());
        material.SetTexture(ShaderConstants._CurveHueVsSat, curves.hueVsSat.value.GetTexture());
        material.SetTexture(ShaderConstants._CurveLumVsSat, curves.lumVsSat.value.GetTexture());
        material.SetTexture(ShaderConstants._CurveSatVsSat, curves.satVsSat.value.GetTexture());
        
        /////////////////UE4_ACES_BEGIN/////////////////
        //HDR Bind UI
        material.SetFloat("FilmSlope", (float)tonemapping.slope);
        material.SetFloat("FilmToe", (float)tonemapping.toe);
        material.SetFloat("FilmShoulder", (float)tonemapping.shoulder);
        material.SetFloat("FilmBlackClip", (float)tonemapping.blackClip);
        material.SetFloat("FilmWhiteClip", (float)tonemapping.whiteClip);
        /////////////////UE4_ACES_END/////////////////

        // Tonemapping (baked into the lut for HDR)
        if (hdr)
        {
            material.shaderKeywords = null;

            switch (tonemapping.mode.value)
            {
                case TonemappingMode.Neutral: material.EnableKeyword(ShaderKeywordStrings.TonemapNeutral); break;
                case TonemappingMode.ACES: material.EnableKeyword(ShaderKeywordStrings.TonemapACES); break;
                /////////////////UE4_ACES_BEGIN/////////////////
                case TonemappingMode.UE4_ACES: material.EnableKeyword(ShaderKeywordStrings.TonemapUE4ACES); break;
                /////////////////UE4_ACES_END/////////////////
                default: break; // None
            }
        }
...
}

ColorGradingLutPass.cs 에 UE4 Aces 인수를 추가하고 키워드를 추가합니다.


부록.

중립적 톤맵핑.

 

racoon-artworks.de/cgbasics/tonemapping.php

 

https://www.racoon-artworks.de/cgbasics/tonemapping.php

Tonemapping Last updated 2019/01/12 Definition Tonemapping is generally understood as compressing a high dynamic range image into a medium that has less dynamic range available. In terms of 3D rendering, this means compressing the realistic, almost unlimit

www.racoon-artworks.de

float3 NeutralTonemap(float3 x, float A, float B, float C, float D, float E,
float F, float WL, float WC)
{
		// Tonemap
		float a = A;
		float b = B;
		float c = C;
		float d = D;
		float e = E;
		float f = F;
		float whiteLevel = WL;
		float whiteClip = WC;
		float3 whiteScale = (1.0).xxx / NeutralCurve(whiteLevel, a, b, c, d, e, f);
		x = NeutralCurve(x * whiteScale, a, b, c, d, e, f);
		x *= whiteScale;
		// Post-curve white point adjustment
		x /= whiteClip.xxx;
		
		return x;
}