【Unity】リニアワークフローにおいてシェーダで扱う値が実際にどう変換されるのか検証する

Unityのリニアワークフローにおいてシェーダで扱う値が実際にどう変わるのか検証します。

リニアワークフロー?

本記事ではリニアワークフローにおいてシェーダで扱う値が実際にどう変わるのかを検証します。
リニアワークフローの基本的な知識については以下の記事にまとめていますので、必要に応じて参照してください。

light11.hatenadiary.com

検証の目的と方法

さてUnityでは色空間としてsRGB空間とリニア空間のどちらかを選択できます。
色空間を変えると色の値の取り扱いが変わり、同じ色でも違う値を示すことになります。

これを検証するため、マテリアルに入力した色やテクスチャの値などがシェーダ内でどのような値に変換されているかを可視化します。

方法としては、まず原点にオブジェクトを配置しておきます。
そして50%グレーを入力値として、頂点シェーダでこの値 x 10だけ座標をずらします。
色空間設定をGammaとLinearで切り替えながら、この結果を検証します。

f:id:halya_11:20200212162257p:plain:w500

実際のシーンは以下のようになります。

f:id:halya_11:20200212133756p:plain:w450
ワイヤーで球が表示されている点が原点、赤い球が頂点シェーダで動かしたモデルです。

これによりシェーダが内部的に扱っている値が可視化されます。
なおわかりやすいように2.2と5.0の位置にPlaneで仕切りを作っています。

ちなみに本記事ではsRGB -> リニア変換のための指数を2.2としていますが、これは近似値となります。
正確には下の記事のような式を用いて変換されている点にご注意ください。

light11.hatenadiary.com

色の入力を検証

それではまずは入力した色がシェーダ内部でどのように扱われているかを調べます。
RGB(0.5, 0, 0)の入力値に対して、R値 * 10をx座標として出力します。

色空間がGammmaの場合は以下のように0.5の値がそのまま使われていることがわかります。

f:id:halya_11:20180210162900p:plain:w450

これに対してLinearの場合は以下のように約0.22に変換されていることがわかります。

f:id:halya_11:20180210163026p:plain:w450

Unityでは色は常にsRGB空間の値が入力されているものとして取り扱われるためこの挙動になります。

テクスチャを検証

テクスチャも同様の手順で検証します。
RGB(0.5, 0, 0)の単色のテクスチャを用意し、頂点シェーダでサンプリングした値を用います。

色空間がGammmaの場合は以下のように0.5の値がそのまま使われていることがわかります。

f:id:halya_11:20180210165115p:plain:w450

これに対してLinearの場合は以下のように約0.22に変換されていることがわかります。

f:id:halya_11:20180210165126p:plain:w450

前節と同様の結果となりました。

ただし、テクスチャに関してはノーマルマップなど、リニア空間で作成したデータをそのまま読み込みたいケースもあります。
このような場合には、Texture ImporterのsRGBのチェックを外すと、テクスチャデータの色空間をLinearとして取り扱えます。

f:id:halya_11:20200212134543p:plain

ライトのIntensityを検証

最後に、ライトのIntensityの値を0.5にして同様に検証します。
ライトのIntensityの値は、ディレクショナルライトの色をRGB(1,1,1)にした上で、シェーダで _LightColor0.r で取得しています。
そのため上記2つの検証時とライティングが変わっていますが、その点は気にしないでください。

色空間がGammmaの場合は以下のように0.5の値がそのまま使われていることがわかります。

f:id:halya_11:20180210170630p:plain:w450

これに対してLinearの場合は以下のように約0.22に変換されていることがわかります。

f:id:halya_11:20180210170716p:plain:w450

ライトのIntensityに関しても色やsRGB設定のテクスチャと同じ結果が得られました。

ただ下の記事にもありますが、ライトのIntensityがこのように変換されるのは特殊な仕様とも言えます。
なぜならリニアワークフローで物理ベースレンダリングを行うときに、
輝度0.5としたい場合に、それを(1/2.2)乗した値である約0.73の値を入力しなければならないためです。

technorgb.blogspot.jp

ただシェーダで取り扱う値はライトの色にIntensityを乗算した状態で渡されるため、
もしIntensityをリニアな値で入力すると今度は色もリニアな値で入力しなければならなくなります。
このような理由からIntensityの入力値はsRGB空間の値として取り扱われているものと思われます。

検証用シェーダ

最後に検証に使ったシェーダを貼っておきます。

色の検証用

Shader "Test_LWF"
{
    Properties
    {
        _Color("Color", Color) = (1,0,0,1)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };

            float4 _Color;
            
            v2f vert (appdata v)
            {
                v2f o;
                v.vertex.x += _Color.r * 10;
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                return half4(1, 0, 0, 1);
            }
            ENDCG
        }
    }
}

テクスチャの検証用

Shader "Test_LWF"
{
    Properties
    {
        _MainTex ("Base (RGB)", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float4 texcoord : TEXCOORD;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };
            
            sampler2D _MainTex;
            float4 _MainTex_ST;
            
            v2f vert (appdata v)
            {
                v2f o;
                float4 tex = tex2Dlod (_MainTex, float4(v.texcoord.xy, 0.0, 0.0));
                v.vertex.x += tex.r * 10;
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                return half4(1, 0, 0, 1);
            }
            ENDCG
        }
    }
}

ライトの検証用

Shader "Test_LWF"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float4 texcoord : TEXCOORD;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };
            
            float4 _LightColor0;
            
            v2f vert (appdata v)
            {
                v2f o;
                v.vertex.x += _LightColor0.r * 10;
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                return half4(1, 0, 0, 1);
            }
            ENDCG
        }
    }
}

関連

light11.hatenadiary.com

light11.hatenadiary.com

参考

technorgb.blogspot.jp

www.slideshare.net