ShaderTips

シェーダーTips

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

リニアワークフロー 概念の説明とUnityにおける実装と設定

視覚のガンマ

下の画像を見てください。

左から右に行くにつれて、白から黒へとグラデーションしており、人間の目で見た輝度はリニア(線形)になっています。 つまり黒の明るさを0、白の明るさを1とすると、中間の明るさは0.5に見えます。

https://cdn-ak.f.st-hatena.com/images/fotolife/h/halya_11/20200210/20200210231541.png リニアワークフロー入門 簡単な説明とその意義 - LIGHT11引用

しかし、実際の輝度は中間の明るさは約0.22です。 見た目と実際の輝度には差があり、人間の目に届くまで約0.45乗されています。

https://cdn-ak.f.st-hatena.com/images/fotolife/h/halya_11/20200210/20200210232617.png リニアワークフロー入門 簡単な説明とその意義 - LIGHT11引用

見た目の輝度と実際の輝度のグラフです。 リニア(直線)ではなく、ガンマカーブと呼ばれる曲線が描かれます。 f:id:Ny_Program:20210819104636p:plain

ディスプレイガンマ

ディスプレイには受け取った輝度を2.2乗してしまう性質があります。 これをガンマ補正と言います。

また、ガンマ補正する数値(2.2)のことをガンマ値と言います。

https://tech.cygames.co.jp/wp-content/uploads/2015/08/2015-08-21_gamma1.png 物理ベースレンダリング -リニアワークフロー編 (1)- | Cygames Engineers' Blog引用

このままだと暗くなってしまうので、予めガンマ値の逆数を累乗(1/2.2=約0.45乗)しておくことで、ガンマ補正を打ち消します。 これをデガンマと言います。

https://tech.cygames.co.jp/wp-content/uploads/2015/08/2015-08-21_gamma2.png 物理ベースレンダリング -リニアワークフロー編 (1)- | Cygames Engineers' Blog引用

リニア色空間

リニア色空間とは中間の明るさが0.5の色空間です。 数値的にリニアですが、物理的には間違えている色空間です。

https://cdn-ak.f.st-hatena.com/images/fotolife/h/halya_11/20200210/20200210231541.png リニアワークフロー入門 簡単な説明とその意義 - LIGHT11引用

sRGB色空間

sRGB色空間とは中間の明るさが約0.22の色空間です。 輝度が1/2.2=約0.45乗されているので、数値的にノンリニアですが、物理的に正しい色空間です。

https://cdn-ak.f.st-hatena.com/images/fotolife/h/halya_11/20200210/20200210232617.png リニアワークフロー入門 簡単な説明とその意義 - LIGHT11引用

デザイナーが見ているモニターはsRGB色空間なので、デザイナーが手書きした画像もsRGB色空間です。

https://4.bp.blogspot.com/-3ShSfgxoPsU/UnyHW6sfF8I/AAAAAAAAaic/qVLrZIciaUE/s800/illustrator.png

sRGB色空間の画像は輝度が1/2.2乗されているので(デガンマされている)、ディスプレイガンマで2.2乗されることで結果的にリニア色空間になります。

https://tech.cygames.co.jp/wp-content/uploads/2015/09/sRGB_Linear.png 物理ベースレンダリング -リニアワークフロー編 (2)- | Cygames Engineers' Blog引用 しかし、人間の目に届く時には視覚のガンマで約0.45乗されます。

エンジニアが意識すること

デザイナーが作成した画像などのsRGB色空間はディスプレイガンマと相殺させることができるという説明をしました。 逆にリニア色空間のライトはディスプレイガンマが掛かって色が暗くなってしまうので、対策をしなければなりません。

また、sRGB色空間とリニア色空間は色空間が違うため、[sRGB色空間の画像カラー * リニア色空間のライトカラー]のように一緒に計算できません。 sRGB色空間をリニア色空間に変換してから、計算する必要があります。 どちらの色空間に合わせても良いのですが、計算しやすいようにリニア色空間にします。 その上で、計算結果をディスプレイガンマと相殺させるために、sRGB色空間に戻す必要があります。 これがリニアワークフローです。

物理ベースレンダリングでどれだけ物理的にライティング計算を行っても、結果を正しく出力できなければ意味がありません。

実装と解説

Unityでリニアワークフローを実装しました。 球体にテクスチャを貼り付け、簡単なライティングを行なっています。 左がガンマワークフロー(従来の何もしない手法)で右がリニアワークフローです。 ガンマワークフローではディスプレイガンマがかかっていて、暗くなっていますが、リニアワークフローではデガンマしているので、暗くなっていません。 f:id:Ny_Program:20210828122335p:plain

以下、シェーダーコードです。

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色空間 → リニア色空間

紫の枠の部分です。

f:id:Ny_Program:20210828131311p:plain 分かる!リニアワークフローのコンポジット - コンポジゴク

      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で、ガンマ(デフォルト)にすると有効になり、リニアを選択すると無効になります。

f:id:Ny_Program:20210827135855p:plain:w500

コードを載せていますが、実はUnityの設定だけでリニアワークフローは実現できます。

ColorSpaceをリニアにしておき、テクスチャ設定でsRGBにしておけば、UNITY_COLORSPACE_GAMMA内の処理と同じようなことをやってくれます。 f:id:Ny_Program:20210828132127p:plain

今回はリニアワークフローを理解するために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

リニア色空間でライティング計算

f:id:Ny_Program:20210828134848p:plain 分かる!リニアワークフローのコンポジット - コンポジゴク

half lambert = max(0,dot(i.normal,WorldSpaceLightDir(i.vertex))) * _Intensity;
fixed4 retColor = albedo * lambert;

リニア色空間でライティングの計算を行います。 ライトはリニア色空間なのでそのまま計算に使います。

リニア色空間→sRGB色空間

f:id:Ny_Program:20210828135215p:plain 分かる!リニアワークフローのコンポジット - コンポジゴク

#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乗しています。

ディスプレイガンマ

f:id:Ny_Program:20210828141311p:plain 分かる!リニアワークフローのコンポジット - コンポジゴク 輝度を2.2乗して出力します。

sRGB色空間で出力したので、リニア色空間になります。

視覚のガンマ

f:id:Ny_Program:20210828141118p:plain 分かる!リニアワークフローのコンポジット - コンポジゴク

最初に紹介し、置き去りにされた視覚のガンマがかかります。

輝度が1/2.2乗されて目に届きます。

普段私たちは視覚のガンマがかかった状態なので、視覚のガンマに対して、デガンマは行いません。

参考

compojigoku.blog.fc2.com tech.cygames.co.jp tech.cygames.co.jp docs.unity3d.com r-ngtm.hatenablog.com

www.slideshare.net