【Unity ガンマ補正】フェードアニメーションの色の歪みについて考えてみる

2022/03/22に公開

はじめに

フェードアニメーションの色の歪みについて考えてみたいと思います。

検証1: RGBカラーをリニアに変化させる

ImageのRGBを黒から白へ、直線的に変化させるようなアニメーションを考えてみます。

C#で書いた場合は以下のようになります。

using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// RGBアニメーション
/// </summary>
public class RGBAnimation : MonoBehaviour
{
    [SerializeField] private RawImage _image;
    private float _delay = 0.5f;
    private float _duration = 1f;

    void Update()
    {
        float t = (Time.time - _delay) / _duration;
        float value = Mathf.Clamp01(t);
        _image.color = new Color(value, value,value, 1f);
    }
}

アニメーションを再生してみる

Other Settings の Color Space = Gamma とした状態で、アニメーションを再生してみましょう。

以下のようなアニメーションになります。
https://www.youtube.com/watch?v=o0CEgrxKSjw

色の変化がおかしい?

ここでアニメーションをよく見てみてください。
色の変化に緩急が付いているように見えませんか?

アニメーションをゆっくり再生すると以下のようになります。
https://youtu.be/Qpt7NupErtw

RGBを上げても、Imageは最初はゆっくりと明るくなり、少し遅れてから勢いよく白へと変化しているように見えると思います。

実はこれ、液晶モニター側で補正がかかるため、色が暗くなって見えるのです。

この現象は、Color Space = Linear の場合でも発生します。

ガンマ補正

色が自然に変化するようにガンマ補正を入れたものを右に並べてみました。
ガンマ補正を入れたほうは、色がリニアに変化しているように見えると思います。
https://youtu.be/RzNXD9uI15Q

C#でのガンマ補正

C#上でガンマ補正を行う場合、Mathf.LinearToGammaSpaceを使用します。

// リニアに変化する値
float t = (Time.time - _delay) / _duration;
float value = Mathf.Clamp01(t); 

// ガンマ補正 (リニア -> ガンマ)
value = Mathf.LinearToGammaSpace(value);

_image.color = new Color(value, value, value, 1f);

シェーダーでのガンマ補正

シェーダー上でガンマ補正を行いたい場合は、LinearToGammaSpaceを使用します。

half3 sRGB = LinearToGammaSpace(color);
UnityCG.cginc
inline half3 LinearToGammaSpace (half3 linRGB)
{
    linRGB = max(linRGB, half3(0.h, 0.h, 0.h));
    // An almost-perfect approximation from http://chilliant.blogspot.com.au/2012/08/srgb-approximations-for-hlsl.html?m=1
    return max(1.055h * pow(linRGB, 0.416666667h) - 0.055h, 0.h);

    // Exact version, useful for debugging.
    //return half3(LinearToGammaSpaceExact(linRGB.r), LinearToGammaSpaceExact(linRGB.g), LinearToGammaSpaceExact(linRGB.b));
}

ガンマ補正を入れる理由

RGBをそのまま画面に表示させた場合、液晶ディスプレイ上では暗く見えます。

RGBを明るく補正してから画面上に表示させることで、液晶モニター上では元の色が表示されているように見えます。
明るく補正することを ガンマ補正と呼びます。

検証2: アルファフェードを考えてみる

次に、アルファ値を利用したフェードアニメーションを考えたいと思います。
アルファを0から1へ線形に変化させるようなアニメーションを組みます。

using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// Alphaアニメーション
/// </summary>
public class AlphaAnimation : MonoBehaviour
{
    [SerializeField] private RawImage _image;
    [SerializeField] private float _delay = 0.5f;
    [SerializeField] private float _duration = 2f;

    void Update()
    {
        // リニアに変化する値
        float t = (Time.time - _delay) / _duration;
        float value = Mathf.Clamp01(t);

        // アルファとして設定
        _image.color = new Color(1f, 1f,1f, value);
    }
}

アニメーションを再生 (Color Space = Gamma)

アニメーションを再生すると、こちらも緩急がついて見えます。(Color Space = Gammaの場合)
https://youtu.be/ShIbFv4i6Hk

イメージの色を黒にした場合も、アルファに緩急がついて見えます。
(アルファが0.7を超えたあたりから、背景がほぼ見えなくなっています)
https://youtu.be/FQwZ1mj-Ipk

Color Space = Linear

Color Space = Linear とすると、緩急は解消されます。
https://youtu.be/N6V7MyOyaR4

イメージの色を黒とした場合も、リニアに変化して見えます。
https://youtu.be/aA_azG1VR-Q

アルファをガンマ補正をするのは誤り

RGBカラーフェード時、RGBにガンマ補正を適用することで色変化の緩急が解消されました。

// ガンマ補正
value = Mathf.LinearToGammaSpace(value);

_image.color = new Color(value, value, value, 1f);

「アルファもガンマ補正すれば、アルファフェードの歪みは解消するのでは?」と考えて、
アルファにガンマ補正を適用しまうと、これは誤りとなります。

// ガンマ補正(誤り)
alpha = Mathf.LinearToGammaSpace(alpha);
        
_image.color = new Color(1f, 1f,1f, alpha);

ガンマ補正とは、モニター側のRGBカラーの歪み(逆ガンマ補正)を打ち消すための補正処理であり、
アルファにガンマ補正を適用するのは誤りとなります。

アルファにガンマ補正を適用したらどうなるか?

アルファにガンマ補正を適用した場合、どのように色が変化するのかを見てみたいと思います。
Unityの Color Space = Gamma とします。

背景(黒) + イメージ(白) の場合

背景を黒、イメージを白にした場合は以下のようになります。
一見、正しく補正されているように見えます。
(左は元のアルファフェード、右がガンマ補正を適用したアルファフェード)
https://www.youtube.com/watch?v=l3O0tCGfrx4

背景(白) + イメージ(黒) の場合

次に、背景を白、イメージを黒色にした状態でアニメーションを再生してみます。
ガンマ補正をすることで、不自然な緩急がつくようになってしまいました。
https://youtu.be/APo5oWe-CEw

不透明と半透明で表示されるRGBが異なる

Color Space = Linear の環境を考えます。

不透明カラー

Imageに不透明カラーを設定した場合、Imageに設定したRGBと同じ色が画面上に表示されます。

例えば、RGBA = (127, 127, 127, 255) という不透明カラーを設定した場合、
画面上にも同じ色 RGB = (127, 127, 127) が表示されます。

半透明カラー

Imageに半透明の白色を設定した場合、アルファをガンマ補正したような値が表示されていました。 (背景が黒である場合)
例えば、RGBA = (255,255,255,127) = float4(1,1,1,0.5) を設定した場合、
画面上には RGB = (187,187,187) = float3(0.73,0.73,0.73) が表示されます。
0.5^{1/2.2} \fallingdotseq 0.73なので、アルファをガンマ補正した値がRGBの値になっています。

ImageのColor上で (R, G, B, A) = (1, 1, 1, 0.5) を設定した場合、感覚的には画面に float3(0.5, 0.5, 0.5)が表示されそうですが、
不思議なことに画面上にはガンマ補正された値float3(0.73, 0.73, 0.73)が表示されてしまっています。

Imageのカラーの扱いについて

Image上で設定するカラーは sRGBカラーとして扱われていると考えられます。

Color Space = Linear の環境では、
sRGBカラーは、シェーダー上で利用される際にLinearに変換されます。
シェーダーの出力は、再びsRGBに変換され、画面に書きこまれます。

uGUIの色の計算

uGUIの色の計算は以下のようになると考えられます。 (モニターの\gamma=2.2の場合)

FinalColor = (Alpha * Color ^ {2.2} + (1 - Alpha) * (BgColor ^ {2.2})) ^ {1/2.2}

Color : ImageのColor
Alpha : Imageのアルファ
BgColor : 背景色

不透明描画の場合

uGUIが不透明描画 (Alpha=1)である場合、以下のような式になります。

FinalColor = (Color^{2.2}) ^ {1/2.2} = Color

入力カラーが、そのまま画面上のカラーになります。

半透明描画の場合

背景が黒、ImageのColorが白である場合、以下のような式になります。
(Color = 1, BgColor = 0 を代入)

\begin{aligned} FinalColor & = (Alpha + (1 - Alpha) * 0) ^ {1/2.2} \\ & = Alpha^{1/2.2} \end{aligned}

背景を黒、イメージを白とした場合、アルファをガンマ補正した値がたまたまRGBとして表示されていたようです。

まとめ

ImageのRGBを変化させた場合

ImageカラーのRGBをリニアに変化させた場合、Color Space = Linear, Gamma の両方でも色変化に緩急がついて見える。
(Imageカラーで設定したRGB値と、画面上に表示されるRGB値が等しくなるため)

// リニアに変化する値
float t = (Time.time - _delay) / _duration;
float value = Mathf.Clamp01(t); 

// RGBとして設定
_image.color = new Color(value, value, value, 1f);

解決策

RGBにガンマ補正を適用することで、見た目上はリニアに変化して見えるようになります。

// リニアに変化する値
float t = (Time.time - _delay) / _duration;
float value = Mathf.Clamp01(t); 

// ガンマ補正 (リニア -> ガンマ)
value = Mathf.LinearToGammaSpace(value);

_image.color = new Color(value, value, value, 1f);

Imageのアルファを変化させた場合

Imageカラーのアルファを0から1へリニアに変化させた場合

  • Color Space = Gamma 環境では、色変化に緩急がついて見える
  • Color Space = Linear 環境では、色がリニアに変化して見える
// リニアに変化する値
float t = (Time.time - _delay) / _duration;
float value = Mathf.Clamp01(t); 

// アルファとして設定
_image.color = new Color(color.r, color.g, color.b, value);

アルファにガンマ補正を適用すると、色変化がおかしくなる

// リニアに変化する値
float t = (Time.time - _delay) / _duration;
float value = Mathf.Clamp01(t); 

// ガンマ補正 (誤り)
value = Mathf.LinearToGammaSpace(value);

// アルファとして設定
_image.color = new Color(color.r, color.g, color.b, value);

Discussion