SPARKCREATIVE Tech Blog

https://www.spark-creative.jp/

UnityのGameViewとSceneViewの色の違い

こんにちはエンジニアの中島悟です。
今回は実際にご相談をいただいたケースをもとにして、UnityのURPの色の話をしようと思います。

それは UnityのURP環境のエディタで、GameViewとSceneViewでVFXの色が違って見える というものでした。

TL;DR

いきなりですが本文が長くなってしまったので結論を書いておきます。

  • HDRのURPではレンダーターゲットのフォーマットの関係で理想とは若干違う色が出る可能性がある
  • SceneViewは作業用のビューなので、GameViewの色を信頼するべき
    • なぜならGameViewは実際にアプリで使われるフォーマットになっているので

調査

さて、色が違って見えるという現象の原因はいくつか考えられます。

  • SPARKGEARが関係するどこかでおかしくなっている
  • 他所のポストエフェクトなどでおかしくなっている
  • どこかの設定が想定と違うものになっている
  • 数値上は同一だが実機と開発機など異なる環境で違うように見えている

大きく分けるとこのような感じでしょうか。
ところがこのケースでは同じ環境どころか1つのエディタ上で色が違うという前提からして全く予想外のものでした。

そこでとりあえず、まずはSPARKGEARが原因かどうかを探るためにUnlitシェーダーのマテリアルを使ってこんなシーンを用意しました。

GameViewとSceneViewに同じものだけを表示したいというシーンです。
そしてGameViewとSceneViewを並べてみると…

よく見ると中間色のものの中に、わずかに色が違っているように見えるものがあります。
スクリーンキャプチャでピクセルの色を調べてみると、0~255の表記で1、2くらいの誤差がありました。

この状態で違いがあるなら、まずはSPARKGEARが原因ではなさそうということで一安心です。 とはいえ弊社製品のせいではありませんでした、だけではご納得いただけないので原因を特定しておかなくてはいけません。

というわけでまずはUnityで Load RenderDoc を実行してレンダリング処理をRenderDocで見てみることにします。 エディタ上でウィンドウを分割してGameViewとSceneViewを両方表示し、RenderDocでキャプチャします。

2つのViewを同時にキャプチャできると上のようになります。 この時は前半がGameViewで、後半がSceneViewの処理になっていました。 そしてそれぞれのレンダリングをしているところを比較してみると、ポリゴンがレンダリングされた時点ですでに色が違っていました。
この段階で違うならここに何かあるはず、とよくよく見ると違いがありました。レンダーターゲットのフォーマットです。

上がGameView、下がSceneViewのレンダーターゲットの情報です。 GameViewのレンダーターゲットのフォーマットはR11G11B10_FLOATですが、SceneViewのレンダーターゲットのフォーマットはR16G16B16A16_TYPELESS Viewd as Floatです。

つまり量子化ビット数の違いによる誤差が、色の違いになっていたといえそうです。

裏取り

URPはソースコードが公開されているので内部の処理を調べることができます。

Runtime/UniversalRenderPipelineCore.csのCreateRenderTextureDescriptorメソッドで選択されたGraphicsFormatのログを出してみると次のようになりました。

SceneCamera has targetTexture. format: R16G16B16A16_SFloat
Main Camera has no targetTexture. choosed format: B10G11R11_UFloatPack32

SceneViewのカメラにはあらかじめtargetTextureがセットされていて、そのフォーマットはR16G16B16A16_SFloatでした。 SceneViewは選択状態やギズモなどを色々と追加で描き込むので余裕のあるフォーマットを選んでいるのかもしれません。

一方GameViewのMain Cameraは自分で設定しない限りはtargetTextureはなく、UniversalRenderPipelineAssetで設定されたフォーマットが使われます。 その設定はRuntime/Data/UniversalRenderPipelineAsset.csにある m_HDRColorBufferPrecision というメンバで、デフォルト値は HDRColorBufferPrecision._32Bits になっています。 そしてHDRColorBufferPrecisionの定義は次のようなものです。

    public enum HDRColorBufferPrecision
    {
        /// <summary> Typically R11G11B10f for faster rendering. Recommend for mobile.
        /// R11G11B10f can cause a subtle blue/yellow banding in some rare cases due to lower precision of the blue component.</summary>
        [Tooltip("Use 32-bits per pixel for HDR rendering.")]
        _32Bits,
        /// <summary>Typically R16G16B16A16f for better quality. Can reduce banding at the cost of memory and performance.</summary>
        [Tooltip("Use 64-bits per pixel for HDR rendering.")]
        _64Bits,
    }

コメントにあるように32BitsはだいたいR11G11B10fで、パフォーマンスの観点からこちらがお勧めなようです。
おまけに今回の話題の色の問題についても書かれています🤣

そしてEditorスクリプトのHDR関連の部分を見ると、この値はいまのところインスペクタに表示される状態にならないようです。したがって、使われるフォーマットは基本的にR11G11B10fということになります。

実験

ところで実際にどのくらい異なるのでしょうか。

2つのカメラにそれぞれのフォーマットのRenderTextureを設定して比較してみました。

シーンのセットアップはこのようになりました。ポストプロセスは有効ですができるだけパススルーに近い設定にしています。

それぞれのRenderTargetの描画が済んだ後で、比較用のマテリアルをセットしたRawImageで差分を表示します。

比較用のマテリアルのシェーダーはShader GraphでSprite Unlitのシェーダーとして作成しました。 内容は差分を取るだけのシンプルなものです。わかりやすくなるように結果にバイアスを乗算できるようになっています。

余談ですが今のところ(Unity 2021.3.24現在)ShaderGraphにはuGUI用のシェーダーを作る設定はありませんが、Sprite UnlitならどうにかuGUIのシェーダーとして使えます。 とはいえ今回のようにただ表示するくらいなら問題ありませんが機能的には不十分なので、本格的に使いたい時はソースコードの形で書き出してステンシル設定などを追記しないといけません。

そして表示してみるとこのようになりました。差分は8倍の値を表示しています。

bit数が少ないので階調が荒くなるのは想像がつきますが、そもそもの発色自体に意外と誤差があるようです。
特に青い色が多く出ていることから、10bitしかない青(B)成分の誤差が多くなっていることが見て取れます。

わずかとはいえ青成分の値だけずれがあると、色が黄か青に転んでしまいます。 デザイナーの方は色にこだわりがあるでしょうし、そもそも発色の良いディスプレイを使っていたりするのでわずかな色の違いでも気になるかもしれません。

せっかくなので基本的なポストプロセスを有効にしてみました。誤差がより大きくなり、ほぼ全体に差分が出ています。

正直なところ想像していたよりもだいぶ誤差が大きくて驚きです。 ですがむしろまんべんなく全体が違っているため分かりにくいようにも思えます。

対策

それならフォーマットをどちらかにそろえることはできないのか、と調べてみるとできなくはありません。

Android または iOS の PlayerSettings の Resolution and Presentation に Render Over Native UI というオプション があります。
説明にあるように、これはAndroid/iOSのネイティブUIの上にUnityのビューを重ねたい時に使うオプションです。

このオプションのオン/オフは PlayerSettings.preserveFramebufferAlpha および Graphics.preserveFramebufferAlpha に対応していて、URPではレンダーターゲットにアルファチャンネルが必要かどうかの設定として使われます。

したがってこれを有効にすると、アルファの使えるHDRフォーマットとしてGameViewのフォーマットが64bit(R16G16B16A16_Float)になります。

Android/iOSのプレイヤー設定にある項目ですが、このオプションはそれ以外のプラットフォームにもそのまま反映されます。

ただ当然ながらビルドしたアプリにも反映されるため、パフォーマンスへの影響が懸念されます。

ご利用は計画的に、という感じです。

まとめ

以上がレンダーターゲットのフォーマットによっては理想とは若干違う色が出る可能性がある、という話でした。
またSceneViewはあくまで作業用のビューなので、色味を確認するのはGameViewの色を信頼するのが良いと言えそうです。

長々お付き合いいただきありがとうございました。それではまた。