RGB値を直接フラグメントシェーダーで出力すると明るい色になってしまう原因と解決方法
概要
カラーパレットでRGB値を指定して色を表示するのと同じように、フラグメントシェーダー内でRGB値を指定して色を表示する方法を紹介します。
指定した色を出力する前に、色空間をsRGB色空間からリニア色空間に変換する必要があります。
問題
R=1.0, G=0.251, B=0.0, A=1.0 (#FF4000)の色を、以下の2通りの方法で表示させてみます。
- Unlit/Color Shaderで色表示
- Unlit Shaderのフラグメントシェーダーの出力で直接RGBの値を指定して色表示
すると、何故かフラグメントシェーダーの出力で直接RGBの値を指定した結果が明るい色になってしまいます。
原因調査
先ほどと同じ色(R=1.0, G=0.251, B=0.0, A=1.0)のテクスチャを用意し、フラグメントシェーダー内でTex2Dを使って各色成分の値を確認してみると、G(緑)の値がテクスチャの緑色成分である0.251ではなく0.052734になっていました。
どうやら、カラーパレットやテクスチャで指定したRGBの値は、フラグメントシェーダーに入る前になにかしら変換されているようでした。
※変数の値(数値)を表示するShaderとその作り方はこちら
原因
Unityでは、カラーパレットやテクスチャで指定した色は、フラグメントシェーダーで処理される前に「sRGB色空間」から「リニア色空間」に色空間の変換がされているそうです。
なので、フラグメントシェーダー内で自分で色を定義・表示させてしまうと、sRGB色空間の色をあたかもリニア色空間の色として表示してしまい、結果として想定していた色よりも明るい色が表示されてしまいます。
原因調査の章で「テクスチャで指定した色成分」と「Tex2Dを使って取得した色成分」が違っていたのも、単に色空間の「変換前の色成分」と「変換後の色成分」の違いを見ていただけです。
色空間の変換についてわかりやすく解説されているサイト様
解決方法
フラグメントシェーダー内で定義した色をsRGB色空間からリニア色空間に自分で変換します。
調べると、sRGB色空間からリニア色空間への色の変換には正式な変換式があるのですが、今回はユーザーのマシンスペックの差が激しいVRChatでの使用を想定しているため、処理の軽い、RGB値を単純に2.2乗する近似式を使います。
近似式は以下のサイト様を参考にしました。
(モニターの歴史的背景などから色空間の変換について解説されています)
問題の章で取り上げた、R=1.0, G=0.251, B=0.0, A=1.0 (#FF4000)の色を、「sRGB色空間」から「リニア色空間」に色空間の変換を行い出力するフラグメントシェーダーです。
fixed4 frag (v2f i) : SV_Target
{
half3 Col = half3(1.0, 0.251, 0.00); // 色成分RGB
return fixed4(pow(Col, 2.2), 1.0); // 色成分RGBを2.2乗して色空間を近似変換して出力
}
実際に表示される色を確認すると、
カラーパレットでRGBの値を指定した場合と大体同じ色を表示させられました!
この問題に直面したきっかけ
2023年8月にFreeCADの構造解析結果モデルをVRChat(Unity)に持ち込む手順を公開しました↓
この商品の仕組みとして、Blenderで構造解析結果の応力値をメッシュの頂点カラーとして保存しておき、UnityのShaderでその応力値から応力色を計算・表示させています。
その過程で、下図のように応力色がどうしてもFreeCADに比べて明るい色になってしまう症状に遭遇し、色々調べた結果フラグメントシェーダー内で新しく定義した色を表示させるには、色空間の変換が必要なことがわかり、今回の記事としてまとめました。
Discussion