【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の設定(HDRかLDR)によって、LutBuilderHdr.shader
かLutBuilderLdr.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.shader
はUberという名の通り、色んなポストエフェクトがひとつのシェーダーで実行されます。
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をユーザーが用意して使うための機能の処理です。推奨されていないそうです。