リニアワークフロー 概念の説明とUnityにおける実装と設定
視覚のガンマ
下の画像を見てください。
左から右に行くにつれて、白から黒へとグラデーションしており、人間の目で見た輝度はリニア(線形)になっています。 つまり黒の明るさを0、白の明るさを1とすると、中間の明るさは0.5に見えます。
しかし、実際の輝度は中間の明るさは約0.22です。 見た目と実際の輝度には差があり、人間の目に届くまで約0.45乗されています。
見た目の輝度と実際の輝度のグラフです。 リニア(直線)ではなく、ガンマカーブと呼ばれる曲線が描かれます。
ディスプレイガンマ
ディスプレイには受け取った輝度を2.2乗してしまう性質があります。 これをガンマ補正と言います。
また、ガンマ補正する数値(2.2)のことをガンマ値と言います。
このままだと暗くなってしまうので、予めガンマ値の逆数を累乗(1/2.2=約0.45乗)しておくことで、ガンマ補正を打ち消します。 これをデガンマと言います。
リニア色空間
リニア色空間とは中間の明るさが0.5の色空間です。 数値的にリニアですが、物理的には間違えている色空間です。
sRGB色空間
sRGB色空間とは中間の明るさが約0.22の色空間です。 輝度が1/2.2=約0.45乗されているので、数値的にノンリニアですが、物理的に正しい色空間です。
デザイナーが見ているモニターはsRGB色空間なので、デザイナーが手書きした画像もsRGB色空間です。
sRGB色空間の画像は輝度が1/2.2乗されているので(デガンマされている)、ディスプレイガンマで2.2乗されることで結果的にリニア色空間になります。
物理ベースレンダリング -リニアワークフロー編 (2)- | Cygames Engineers' Blog引用 しかし、人間の目に届く時には視覚のガンマで約0.45乗されます。
エンジニアが意識すること
デザイナーが作成した画像などのsRGB色空間はディスプレイガンマと相殺させることができるという説明をしました。 逆にリニア色空間のライトはディスプレイガンマが掛かって色が暗くなってしまうので、対策をしなければなりません。
また、sRGB色空間とリニア色空間は色空間が違うため、[sRGB色空間の画像カラー * リニア色空間のライトカラー]のように一緒に計算できません。 sRGB色空間をリニア色空間に変換してから、計算する必要があります。 どちらの色空間に合わせても良いのですが、計算しやすいようにリニア色空間にします。 その上で、計算結果をディスプレイガンマと相殺させるために、sRGB色空間に戻す必要があります。 これがリニアワークフローです。
物理ベースレンダリングでどれだけ物理的にライティング計算を行っても、結果を正しく出力できなければ意味がありません。
実装と解説
Unityでリニアワークフローを実装しました。 球体にテクスチャを貼り付け、簡単なライティングを行なっています。 左がガンマワークフロー(従来の何もしない手法)で右がリニアワークフローです。 ガンマワークフローではディスプレイガンマがかかっていて、暗くなっていますが、リニアワークフローではデガンマしているので、暗くなっていません。
以下、シェーダーコードです。
Shader "LinearWorkflow" { Properties { _MainTex ("Texture", 2D) = "white" {} _Intensity("Intensity", Range(0,1)) = 0 [Toggle] _IS_LINEAR("Is Linear", Float) = 0 [Toggle] _USE_FAST("Use Fast", Float) = 0 } SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #pragma shader_feature _ _USE_FAST_ON #pragma shader_feature _ _IS_LINEAR_ON struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; half3 normal : NORMAL; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; half3 normal : TEXCOORD1; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; o.normal = UnityObjectToWorldNormal(v.normal); return o; } sampler2D _MainTex; half _Intensity; #define FLT_EPSILON 1.192092896e-07 // Smallest positive number, such that 1.0 + FLT_EPSILON != 1.0 float3 PositivePow(float3 base, float3 power) { return pow(max(abs(base), float3(FLT_EPSILON, FLT_EPSILON, FLT_EPSILON)), power); } half3 sRGBToLinear(half3 c) { half3 linearRGB; #ifdef _USE_FAST_ON linearRGB = pow(c,2.2); #else half3 linearRGBLo = c / 12.92; half3 linearRGBHi = PositivePow((c + 0.055) / 1.055, half3(2.4, 2.4, 2.4)); linearRGB = (c <= 0.04045) ? linearRGBLo : linearRGBHi; #endif return linearRGB; } half4 sRGBToLinear(half4 c) { return half4(sRGBToLinear(c.rgb), c.a); } half3 LinearTosRGB(half3 c) { half3 sRGB; #ifdef _USE_FAST_ON sRGB = pow(c, 1 / 2.2); #else half3 sRGBLo = c * 12.92; half3 sRGBHi = (PositivePow(c, half3(1.0 / 2.4, 1.0 / 2.4, 1.0 / 2.4)) * 1.055) - 0.055; sRGB = (c <= 0.0031308) ? sRGBLo : sRGBHi; #endif return sRGB; } half4 LinearTosRGB(half4 c) { return half4(LinearTosRGB(c.rgb), c.a); } fixed4 frag (v2f i) : SV_Target { fixed4 albedo = tex2D(_MainTex, i.uv); #if UNITY_COLORSPACE_GAMMA && _IS_LINEAR_ON albedo = sRGBToLinear(albedo); #endif half lambert = max(0,dot(i.normal,WorldSpaceLightDir(i.vertex))) * _Intensity; fixed4 retColor = albedo * lambert; #if UNITY_COLORSPACE_GAMMA && _IS_LINEAR_ON retColor = LinearTosRGB(retColor); #endif return retColor; } ENDCG } } }
Propertiesの中身
_Intensity ライトの強度
_IS_LINEAR オンにするとリニアワークフローが有効。オフにするとガンマワークフローになります。
_USE_FAST オンにするとリニアワークフローの近似式を使用します。
sRGB色空間 → リニア色空間
紫の枠の部分です。
fixed4 albedo = tex2D(_MainTex, i.uv); #if UNITY_COLORSPACE_GAMMA && _IS_LINEAR_ON albedo = sRGBToLinear(albedo); #endif
sRGB色空間のテクスチャをリニア色空間に変換しています。
UNITY_COLORSPACE_GAMMAはEdit>ProjectSetting>OtherSetting>Color Spaceで、ガンマ(デフォルト)にすると有効になり、リニアを選択すると無効になります。
コードを載せていますが、実はUnityの設定だけでリニアワークフローは実現できます。
ColorSpaceをリニアにしておき、テクスチャ設定でsRGBにしておけば、UNITY_COLORSPACE_GAMMA内の処理と同じようなことをやってくれます。
今回はリニアワークフローを理解するためにColorSpaceをガンマにした状態でも、リニアワークフローが効くようにします。
half3 sRGBToLinear(half3 c) { half3 linearRGB; #ifdef _USE_FAST_ON linearRGB = pow(c,2.2); #else half3 linearRGBLo = c / 12.92; half3 linearRGBHi = PositivePow((c + 0.055) / 1.055, half3(2.4, 2.4, 2.4)); linearRGB = (c <= 0.04045) ? linearRGBLo : linearRGBHi; #endif return linearRGB; } half4 sRGBToLinear(half4 c) { return half4(sRGBToLinear(c.rgb), c.a); }
sRGBToLinearの中身です。
sRGB→リニア変換を行なっています。
_USE_FAST_ONでは近似式を使用しています。 引数のカラーに対して、2.2乗しています。
#elseの中身はこちらのリポジトリから引用しました。 ご参考にどうぞ。 github.com
リニア色空間でライティング計算
half lambert = max(0,dot(i.normal,WorldSpaceLightDir(i.vertex))) * _Intensity;
fixed4 retColor = albedo * lambert;
リニア色空間でライティングの計算を行います。 ライトはリニア色空間なのでそのまま計算に使います。
リニア色空間→sRGB色空間
#if UNITY_COLORSPACE_GAMMA && _IS_LINEAR_ON retColor = LinearTosRGB(retColor); #endif return retColor;
シェーダー最後のフローです。 リニア色空間からsRGB色空間に変換します。 sRGB色空間にしておくことで、ディスプレイガンマがかかった時にリニアになります。
ここでも、ColorSpaceをリニアにすることで、UNITY_COLORSPACE_GAMMA内の処理と同じようなことをやってくれます。
half3 LinearTosRGB(half3 c) { half3 sRGB; #ifdef _USE_FAST_ON sRGB = pow(c, 1 / 2.2); #else half3 sRGBLo = c * 12.92; half3 sRGBHi = (PositivePow(c, half3(1.0 / 2.4, 1.0 / 2.4, 1.0 / 2.4)) * 1.055) - 0.055; sRGB = (c <= 0.0031308) ? sRGBLo : sRGBHi; #endif return sRGB; } half4 LinearTosRGB(half4 c) { return half4(LinearTosRGB(c.rgb), c.a); }
LinearTosRGBの中身です。
リニア→sRGB変換を行なっています。
引数のカラーに対して、1/2.2乗しています。
ディスプレイガンマ
分かる!リニアワークフローのコンポジット - コンポジゴク 輝度を2.2乗して出力します。
sRGB色空間で出力したので、リニア色空間になります。
視覚のガンマ
最初に紹介し、置き去りにされた視覚のガンマがかかります。
輝度が1/2.2乗されて目に届きます。
普段私たちは視覚のガンマがかかった状態なので、視覚のガンマに対して、デガンマは行いません。
参考
compojigoku.blog.fc2.com tech.cygames.co.jp tech.cygames.co.jp docs.unity3d.com r-ngtm.hatenablog.com