ShaderTips

シェーダーTips

主にUnityシェーダーについての記事を書いています。

【Unity】【URP】LUT (ルックアップテーブル)

カラーグレーディング(Color Grading) とは

画面の色合いを調整するポストエフェクトです。

LUTとは

ルックアップテーブル(Look Up Table)の略称で、入力されたRGBを指定した別のRGBに変換するための情報が格納されてたテクスチャです。 カラーグレーディングで使われます。

URPにおけるカラーグレーディング

設定

Grading Mode

LUTテクスチャのグラフィックフォーマットをLDR/HDRのいずれにするかを指定します。

LUT size

LUTテクスチャのサイズを設定します。設定した値Aに対し「高さA、幅A×A」 サイズのテクスチャが生成されます。

LUTの作成

ColorGradingLutPassでLUTを生成するシェーダーを実行します。 以下はExecute関数です。 各カラーグレーディングの設定をシェーダーに渡し、LUTテクスチャをレンダーターゲットとして描画の実行をしています。

public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
    var cmd = CommandBufferPool.Get();
    using (new ProfilingScope(cmd, ProfilingSampler.Get(URPProfileId.ColorGradingLUT)))
    {
        // 各カラーグレーディングの設定はVolumeで行います
        // 追加したVolumeを取得します
        var stack = VolumeManager.instance.stack;
        var channelMixer = stack.GetComponent<ChannelMixer>();
        var colorAdjustments = stack.GetComponent<ColorAdjustments>();
        var curves = stack.GetComponent<ColorCurves>();
        var liftGammaGain = stack.GetComponent<LiftGammaGain>();
        var shadowsMidtonesHighlights = stack.GetComponent<ShadowsMidtonesHighlights>();
        var splitToning = stack.GetComponent<SplitToning>();
        var tonemapping = stack.GetComponent<Tonemapping>();
        var whiteBalance = stack.GetComponent<WhiteBalance>();
        // UniversalRenderPipelineAssetで設定したGrading Modeを取得します
        // Grading ModeはLUTテクスチャのグラフィックフォーマットをLDRまたはHDRのどちらにするかを設定します
        ref var postProcessingData = ref renderingData.postProcessingData;
        bool hdr = postProcessingData.gradingMode == ColorGradingMode.HighDynamicRange;
        // UniversalRenderPipelineAssetで設定したLUTsizeを取得
        // 高さ = LUTsize
        int lutHeight = postProcessingData.lutSize;
        // 幅 = LUTsize^2
        int lutWidth = lutHeight * lutHeight;
        // LDRかHDRによってテクスチャフォーマットを変更
        // LDR : R8G8B8A8_UNorm
        // HDR : R16G16B16A16_SFloat サポートされていなければ、B10G11R11_UFloatPack32
        var format = hdr ? m_HdrLutFormat : m_LdrLutFormat;
        // LDRかHDRによってマテリアルを変更します
        // LDR : LutBuilderLdr.shaderが設定されているマテリアル
        // HDR : LutBuilderHdr.shaderが設定されているマテリアル
        var material = hdr ? m_LutBuilderHdr : m_LutBuilderLdr;
        // LUTを一時テクスチャをして生成します
        var desc = new RenderTextureDescriptor(lutWidth, lutHeight, format, 0);
        desc.vrUsage = VRTextureUsage.None; // We only need one for both eyes in VR
        cmd.GetTemporaryRT(m_InternalLut.id, desc, FilterMode.Bilinear);
        // 各カラーグレーディングの設定から、シェーダーパラメータを設定します
        var lmsColorBalance = ColorUtils.ColorBalanceToLMSCoeffs(whiteBalance.temperature.value, whiteBalance.tint.value);
        var hueSatCon = new Vector4(colorAdjustments.hueShift.value / 360f, colorAdjustments.saturation.value / 100f + 1f, colorAdjustments.contrast.value / 100f + 1f, 0f);
        var channelMixerR = new Vector4(channelMixer.redOutRedIn.value / 100f, channelMixer.redOutGreenIn.value / 100f, channelMixer.redOutBlueIn.value / 100f, 0f);
        var channelMixerG = new Vector4(channelMixer.greenOutRedIn.value / 100f, channelMixer.greenOutGreenIn.value / 100f, channelMixer.greenOutBlueIn.value / 100f, 0f);
        var channelMixerB = new Vector4(channelMixer.blueOutRedIn.value / 100f, channelMixer.blueOutGreenIn.value / 100f, channelMixer.blueOutBlueIn.value / 100f, 0f);
        var shadowsHighlightsLimits = new Vector4(
            shadowsMidtonesHighlights.shadowsStart.value,
            shadowsMidtonesHighlights.shadowsEnd.value,
            shadowsMidtonesHighlights.highlightsStart.value,
            shadowsMidtonesHighlights.highlightsEnd.value
        );
        var (shadows, midtones, highlights) = ColorUtils.PrepareShadowsMidtonesHighlights(
            shadowsMidtonesHighlights.shadows.value,
            shadowsMidtonesHighlights.midtones.value,
            shadowsMidtonesHighlights.highlights.value
        );
        var (lift, gamma, gain) = ColorUtils.PrepareLiftGammaGain(
            liftGammaGain.lift.value,
            liftGammaGain.gamma.value,
            liftGammaGain.gain.value
        );
        var (splitShadows, splitHighlights) = ColorUtils.PrepareSplitToning(
            splitToning.shadows.value,
            splitToning.highlights.value,
            splitToning.balance.value
        );
        var lutParameters = new Vector4(lutHeight, 0.5f / lutWidth, 0.5f / lutHeight,
            lutHeight / (lutHeight - 1f));
        material.SetVector(ShaderConstants._Lut_Params, lutParameters);
        material.SetVector(ShaderConstants._ColorBalance, lmsColorBalance);
        material.SetVector(ShaderConstants._ColorFilter, colorAdjustments.colorFilter.value.linear);
        material.SetVector(ShaderConstants._ChannelMixerRed, channelMixerR);
        material.SetVector(ShaderConstants._ChannelMixerGreen, channelMixerG);
        material.SetVector(ShaderConstants._ChannelMixerBlue, channelMixerB);
        material.SetVector(ShaderConstants._HueSatCon, hueSatCon);
        material.SetVector(ShaderConstants._Lift, lift);
        material.SetVector(ShaderConstants._Gamma, gamma);
        material.SetVector(ShaderConstants._Gain, gain);
        material.SetVector(ShaderConstants._Shadows, shadows);
        material.SetVector(ShaderConstants._Midtones, midtones);
        material.SetVector(ShaderConstants._Highlights, highlights);
        material.SetVector(ShaderConstants._ShaHiLimits, shadowsHighlightsLimits);
        material.SetVector(ShaderConstants._SplitShadows, splitShadows);
        material.SetVector(ShaderConstants._SplitHighlights, splitHighlights);
        material.SetTexture(ShaderConstants._CurveMaster, curves.master.value.GetTexture());
        material.SetTexture(ShaderConstants._CurveRed, curves.red.value.GetTexture());
        material.SetTexture(ShaderConstants._CurveGreen, curves.green.value.GetTexture());
        material.SetTexture(ShaderConstants._CurveBlue, curves.blue.value.GetTexture());
        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());
        // Tonemapping
        if (hdr)
        {
            material.shaderKeywords = null;
            switch (tonemapping.mode.value)
            {
                case TonemappingMode.Neutral: material.EnableKeyword(ShaderKeywordStrings.TonemapNeutral); break;
                case TonemappingMode.ACES: material.EnableKeyword(m_AllowColorGradingACESHDR ? ShaderKeywordStrings.TonemapACES : ShaderKeywordStrings.TonemapNeutral); break;
                default: break; 
            }
        }
        // XR系処理
        renderingData.cameraData.xr.StopSinglePass(cmd);
        // 描画実行
        cmd.Blit(null, m_InternalLut.id, material);
        // XR系処理
        renderingData.cameraData.xr.StartSinglePass(cmd);
    }
    context.ExecuteCommandBuffer(cmd);
    CommandBufferPool.Release(cmd);
}

Grading Modeの設定(HDRLDR)によって、LutBuilderHdr.shaderLutBuilderLdr.shaderのどちらかが実行されます。

今回はLutBuilderLdr.shaderの処理を見ていきます。

SubShader
{
    Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline"}
    LOD 100
    ZTest Always ZWrite Off Cull Off
    Pass
    {
        Name "LutBuilderLdr"
        HLSLPROGRAM
            // Fullscreeen.hlslに定義されている汎用頂点シェーダー
            #pragma vertex FullscreenVert
            #pragma fragment Frag
        ENDHLSL
    }
}

フラグメントシェーダーの定義

half4 Frag(Varyings input) : SV_Target
{
    // ① LUT生成
    float3 colorLinear = GetLutStripValue(input.uv, _Lut_Params);
    float3 colorLMS = LinearToLMS(colorLinear);
    colorLMS *= _ColorBalance.xyz;
    colorLinear = LMSToLinear(colorLMS);
    float3 colorLog = LinearToLogC(colorLinear);
    colorLog = (colorLog - ACEScc_MIDGRAY) * _HueSatCon.z + ACEScc_MIDGRAY;
    colorLinear = LogCToLinear(colorLog);
    // ②Color AdjustmentsのColor filterを適応
    colorLinear *= _ColorFilter.xyz;
    colorLinear = max(0.0, colorLinear);
    // Split toning
    // As counter-intuitive as it is, to make split-toning work the same way it does in Adobe
    // products we have to do all the maths in gamma-space...
    float balance = _SplitShadows.w;
    float3 colorGamma = PositivePow(colorLinear, 1.0 / 2.2);
    float luma = saturate(GetLuminance(saturate(colorGamma)) + balance);
    float3 splitShadows = lerp((0.5).xxx, _SplitShadows.xyz, 1.0 - luma);
    float3 splitHighlights = lerp((0.5).xxx, _SplitHighlights.xyz, luma);
    colorGamma = SoftLight(colorGamma, splitShadows);
    colorGamma = SoftLight(colorGamma, splitHighlights);
    colorLinear = PositivePow(colorGamma, 2.2);
    // Channel mixing (Adobe style)
    colorLinear = float3(
        dot(colorLinear, _ChannelMixerRed.xyz),
        dot(colorLinear, _ChannelMixerGreen.xyz),
        dot(colorLinear, _ChannelMixerBlue.xyz)
    );
    // Shadows, midtones, highlights
    luma = GetLuminance(colorLinear);
    float shadowsFactor = 1.0 - smoothstep(_ShaHiLimits.x, _ShaHiLimits.y, luma);
    float highlightsFactor = smoothstep(_ShaHiLimits.z, _ShaHiLimits.w, luma);
    float midtonesFactor = 1.0 - shadowsFactor - highlightsFactor;
    colorLinear = colorLinear * _Shadows.xyz * shadowsFactor
        + colorLinear * _Midtones.xyz * midtonesFactor
        + colorLinear * _Highlights.xyz * highlightsFactor;
    // Lift, gamma, gain
    colorLinear = colorLinear * _Gain.xyz + _Lift.xyz;
    colorLinear = sign(colorLinear) * pow(abs(colorLinear), _Gamma.xyz);
    // HSV operations
    float satMult;
    float3 hsv = RgbToHsv(colorLinear);
    {
        // Hue Vs Sat
        satMult = EvaluateCurve(_CurveHueVsSat, hsv.x) * 2.0;
        // Sat Vs Sat
        satMult *= EvaluateCurve(_CurveSatVsSat, hsv.y) * 2.0;
        // Lum Vs Sat
        satMult *= EvaluateCurve(_CurveLumVsSat, Luminance(colorLinear)) * 2.0;
        // Hue Shift & Hue Vs Hue
        float hue = hsv.x + _HueSatCon.x;
        float offset = EvaluateCurve(_CurveHueVsHue, hue) - 0.5;
        hue += offset;
        hsv.x = RotateHue(hue, 0.0, 1.0);
    }
    colorLinear = HsvToRgb(hsv);
    // Global saturation
    luma = GetLuminance(colorLinear);
    colorLinear = luma.xxx + (_HueSatCon.yyy * satMult) * (colorLinear - luma.xxx);
    // YRGB curves
    {
        const float kHalfPixel = (1.0 / 128.0) / 2.0;
        float3 c = colorLinear;
        // Y (master)
        c += kHalfPixel.xxx;
        float mr = EvaluateCurve(_CurveMaster, c.r);
        float mg = EvaluateCurve(_CurveMaster, c.g);
        float mb = EvaluateCurve(_CurveMaster, c.b);
        c = float3(mr, mg, mb);
        // RGB
        c += kHalfPixel.xxx;
        float r = EvaluateCurve(_CurveRed, c.r);
        float g = EvaluateCurve(_CurveGreen, c.g);
        float b = EvaluateCurve(_CurveBlue, c.b);
        colorLinear = float3(r, g, b);
    }
    return half4(saturate(colorLinear), 1.0);
}

①GetLutStripValueはcolor.hlslに定義されています。

UVとparams = (lut_height, 0.5 / lut_width, 0.5 / lut_height, lut_height / lut_height - 1)を引数として受け取っています。

real3 GetLutStripValue(float2 uv, float4 params)
{
    uv -= params.yz;
    real3 color;
    color.r = frac(uv.x * params.x);
    color.b = uv.x - color.r / params.x;
    color.g = uv.y;
    return color * params.w;
}

ぱっと見では分かりにくいので、GetLutStripValueの値をそのまま出力してみます。

half4 Frag(Varyings input) : SV_Target
{
    float3 colorLinear = GetLutStripValue(input.uv, _Lut_Params);
    return half4(colorLinear.r,colorLinear.g,colorLinear.b,1);
}

RGB値 2次元の画像が横に並んでいます。

R値 各画像のX値と一致します。

G値 各画像のY値と一致します。

B値 各画像のインデックス値と一致します。

LUTは入力されたRGBを指定した別のRGBに変換するための情報が格納されてたテクスチャと前述しましたが、変換前のカラーRをX、GをY, Bをインデックスと見立てて、LUTのテクスチャをフェッチすることで、色を変換します。 GetLutStripValueの段階では入力カラーは色が変化しません。 その後の処理でLUTを書き換え、後ほど使用します。

②Color AdjustmentsのColor filterで画面を赤くした際はLUTの色が赤くなっています。

LUTを使ってカラーグレーディング

カラーグレーディングはUberPost.shaderで反映されます。 UberPost.shaderUberという名の通り、色んなポストエフェクトがひとつのシェーダーで実行されます。

SubShader
{
    Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline"}
    LOD 100
    ZTest Always ZWrite Off Cull Off
    Pass
    {
        Name "UberPost"
        HLSLPROGRAM
            // Fullscreeen.hlslに定義されている汎用頂点シェーダー
            #pragma vertex FullscreenVert
            #pragma fragment Frag
        ENDHLSL
    }
}

フラグメントシェーダーの定義

half4 Frag(Varyings input) : SV_Target
{
・
・ // 色んなポストエフェクトの処理// カラーグレーディングの適応
   color = ApplyColorGrading(color, PostExposure, TEXTURE2D_ARGS(_InternalLut, sampler_LinearClamp), LutParams, TEXTURE2D_ARGS(_UserLut, sampler_LinearClamp), UserLutParams, UserLutContribution);
・
・ // 色んなポストエフェクトの処理return half4(color, 1.0);
}

ApplyColorGradingの定義

half3 ApplyColorGrading(half3 input, float postExposure, TEXTURE2D_PARAM(lutTex, lutSampler), float3 lutParams, TEXTURE2D_PARAM(userLutTex, userLutSampler), float3 userLutParams, float userLutContrib)
{
    // Color AdjustmentsのPost Exposure 画面の明るさを調整します 2のn乗が入力カラー値の要素に乗算します
    input *= postExposure;

    // Grading ModeでHDRを選択
    #if _HDR_GRADING
    {
        // ①リニア色空間からLogc色空間に変換
        float3 inputLutSpace = saturate(LinearToLogC(input));
        // ②Lutを使ってカラー変換
        input = ApplyLut2D(TEXTURE2D_ARGS(lutTex, lutSampler), inputLutSpace, lutParams);
        
        // ③Color Look Up(ユーザー定義のLUT)の適応率が0以上
        UNITY_BRANCH
        if (userLutContrib > 0.0)
        {
            input = saturate(input);
            input.rgb = GetLinearToSRGB(input.rgb);
            half3 outLut = ApplyLut2D(TEXTURE2D_ARGS(userLutTex, userLutSampler), input, userLutParams);
            input = lerp(input, outLut, userLutContrib);
            input.rgb = GetSRGBToLinear(input.rgb);
        }
    }

    // Grading ModeでLDRを選択
    #else
    {
        // ToneMapの適応
        input = ApplyTonemap(input);

        // ③Color Look Up(ユーザー定義のLUT)の適応率が0以上
        UNITY_BRANCH
        if (userLutContrib > 0.0)
        {
            input.rgb = GetLinearToSRGB(input.rgb);
            half3 outLut = ApplyLut2D(TEXTURE2D_ARGS(userLutTex, userLutSampler), input, userLutParams);
            input = lerp(input, outLut, userLutContrib);
            input.rgb = GetSRGBToLinear(input.rgb);
        }
        //  ②Grading ModeがLDRの場合はLDRでLutに保存されているので、そのままLutを使ってカラー変換
        input = ApplyLut2D(TEXTURE2D_ARGS(lutTex, lutSampler), input, lutParams);
    }
    #endif

    return input;
}

①LUTは基本リニア色空間ですが、Grading ModeでHDRを選択した場合はLUT作成時にLogC色空間(HDR形式のフォーマット)に変換されてから保存されます。 そのため、入力カラーをLogC色空間に変換しています。

②入力カラーからLUTを使って色変換を行います。 ApplyLut2Dは以下のように定義されています。

real3 ApplyLut2D(TEXTURE2D_PARAM(tex, samplerTex), float3 uvw, float3 scaleOffset)
{
    uvw.z *= scaleOffset.z;
    float shift = floor(uvw.z);
    uvw.xy = uvw.xy * scaleOffset.z * scaleOffset.xy + scaleOffset.xy * 0.5;
    uvw.x += shift * scaleOffset.y;
    uvw.xyz = lerp(
        SAMPLE_TEXTURE2D_LOD(tex, samplerTex, uvw.xy, 0.0).rgb,
        SAMPLE_TEXTURE2D_LOD(tex, samplerTex, uvw.xy + float2(scaleOffset.y, 0.0), 0.0).rgb,
        uvw.z - shift
    );
    return uvw;
}

③Color Look UpというLUTをユーザーが用意して使うための機能の処理です。推奨されていないそうです。