DirectXの話 第179回

HDRにおけるUI描画

21/12/25 up

SDRとHDR

Standard Dynamic Range (SDR)High Dynamic Range (HDR) は現在のゲーム開発現場における1つの問題となっていると感じます。
開発現場ではまだまだSDRディスプレイが一般的ですし、HDR対応が行われていない、もしくは微妙なソフトウェアも多いです。

SDRとHDRによって明るさが違うってくると思われがちですが、色空間も違ってきていますので表現できる色の範囲も違います。
HDRは規格によって数値の意味合いも変わってきますし、表現の幅にも違いがあります。
光と電気の変換を行う伝達関数にも違いがあるため、レンダリング結果の表示にも差が出てきます。

現在のレンダリングパイプラインにおけるSDR/HDR対応は以下のようになっているのが一般的だろうと思います。

レンダリング画像まではリニアガンマで処理します。色空間は大体の場合でRec709です。
HDRの場合はこの後にRec709からRec2020に変換することになります。
この後はトーンマップとポストプロセスです。ポストプロセスはトーンマップの前に行われるものもありますが、ここでは省略しています。
そしてOETF (光電伝達関数) を行います。この伝達関数はSDRならいわゆるガンマカーブ、HDRなら規格によってPQやHLGとなります。
その後からはディスプレイ側の処理です。電気信号を光信号に変換するEOTFが実行され、最終的にディスプレイに表示されます。

UI描画の問題

さて、3Dのレンダリングは色空間変換やトーンマップの前で大体の場合終了します。
ポストプロセスはトーンマップの前後ですが、外部データを使うことがなければ特に問題にはならないでしょう。

しかしUI、特にレンダリング画像上に乗せられるHUDのレンダリングには問題が出てきます。
というのも、現在の多くのHUD/UIレンダリングは以下のようになっていることが多いのではないでしょうか。

PhotoshopなどでHUDやUIを作成する場合、多くはゲーム画面のスクリーンショットにレイヤーを重ねることになるかと思います。
それに合わせることを考慮するとOETF後に処理されることになります。
ただしこれはSDRのガンマカーブがかかった状態に対しての描画ですので、HDRのPQカーブなどがかかった状態とブレンドするとかなり違う結果となります。

まずは色空間。UI用のテクスチャはsRGBですが、OETFがかかっている画像はRec2020なので色空間を合わせる必要があります。
その上明るさの問題も出てきます。
sRGBは規格では80nitsの明るさまでを表現できるので、1.0という値が80nitsです。
これに対してPQカーブは1.0が10,000nitsです。
もしもUIテクスチャをそのままOETF結果に書き込んでしまうとsRGBの80nitsまでのデータのはずが10,000nits最大となってしまいます。
簡単に言うとかなり明るく表示されます。HDRディスプレイで見ると眩しくてヤバい。

しかし、HDRの場合にsRGBのテクスチャをシェーダ内でRec2020に変換、明るさを調整してPQカーブをかければなんとかなるでしょう。
ただし、ブレンドしなければ、という話です。
いわゆる半透明や加算合成が加わるとブレンド結果に違いが出てしまいます。
何より、いちいちUIレンダリングで色空間変換やOETFの計算を行うのもパフォーマンス的に問題が出ます。

UI描画を変更してみる

今までのUI描画では問題が発生することがわかりました。
そこでこのようにレンダリングパスを変更してみます。

OETF前にHUD/UI描画を回すことでOETFによるブレンド結果の大幅な違いを吸収するのが目的です。
トーンマップによる違いは出ますが、そこまで問題にならないと考えられます。

ただしこの方法でもUIレンダリング時にいちいちRec2020に変換する必要があります。
なのでこのように変更してみましょう。

一旦オフスクリーンにHUD/UIを描画します。もちろん半透明や加算合成を考慮した処理を行ってです。
そしてそのオフスクリーンバッファをレンダリング画像にコンポジットします。
この方法であればオフスクリーンバッファへのレンダリングはRec709で行えますし、コンポジットの段階でSDR/HDRの違いによってRec709/Rec2020でコンポジットすればいいわけです。

また、UIの明るさの問題もあります。
先にも書いたとおり、sRGBの明るさは80nitsが限界です。
しかし、現在のSDRディスプレイのほとんどは200nits以上の明るさを持っている場合がほとんどだそうです。
もちろんその明るさを限界まで利用しているというわけではないでしょうけれど、80nitsより大きな値で表示しているのは事実です。

では、これをHDRディスプレイで表示したらどうなるでしょう?
現在のHDRディスプレイは400nitsくらいから最大で1000nitsくらいまでは表示できます。
これが正しい明るさで表示されるとなると、80nitsはかなり暗い状態と言えます。

実際、同じコンテンツをSDRとHDRで表示するとHDRの方が暗くなるとよく言われます。
また、WindowsのHDR設定でも「SDRコンテンツの明るさ」という項目があり、これを操作するとSDRアプリケーションの明るさを変更することができます。
デフォルトでは50となっており、SDRアプリケーションの表示をある程度明るくしている状態で表示しています。
この値を0にすることでSDRアプリケーションを正しい明るさで表示できるのですが、そうするとかなり暗くなります。
これと同じ現象がUIにも発生するということになります。

そのためか、最近はUIの明るさ設定を指定できるゲームも多くあります。
このUIの明るさ設定もUIの各パーツを描画するタイミングよりコンポジットするタイミングのほうがやりやすいでしょう。

と、ここまでの話は結局のところ理論の話です。
このような話をUIデザイナーにしたとしましょう。どうなるでしょう?

「で、結局どうすればいいの?どんなふうに見えるの?」

こう言われるでしょう。
実際、どう見えるかというのを説明できるでしょうか?そもそもどう見えるかを想像できるでしょうか?
私はできません。想像すらもできないので。

というわけで、色んなパターンでのUI描画を実装してみましょう。

サンプルについて

今回は前回のFSR/NISサンプルであるSample027を利用しました。

https://github.com/Monsho/D3D12Samples

HUDについては動きはしませんが、実際のゲームにありそうな体力ゲージ的なものとミニマップ的なものです。
体力ゲージは枠もバーも不透明、文字も不透明ですが、文字にかかるグローは加算合成です。
ミニマップはマップ中央が不透明で円周に広がるにつれて透明度が上がっていきます。
位置表示的な赤い光点も半透明で描画されています。

描画タイミングはOETFの前と後を選択できます。
UI Draw Timing の BeforeOETF/AfterOETF がそれぞれOETFの前後を示しています。
Direct/Indirect は直接バッファにUIパーツを描画するか、オフスクリーンに描画してからコンポジットするかを示します。
Directの場合はRec709/Rec2020を選択でき、HDRの場合にはRec2020のバッファに対してそれぞれの色空間で描画を行います。SDRの場合はRec709限定です。

UI IntensityはUIの明るさを設定できます。
UI AlphaはUI全体のアルファ値を設定できます。

SDR/HDRの変更は起動時引数で行います。
何も指定しなければSDR、"-hdr"を指定するとHDRで起動します。
HDR起動してもディスプレイがHDR対応していないとSDRとなります。マルチディスプレイの環境では最初に見つかったHDRディスプレイの設定を利用します。
可能であれば、SDRとHDRのマルチディスプレイ環境でSDR/HDRの双方で起動して見た目を比較すると良いでしょう。

実際にどのような問題が出るでしょう。
まずはHDRでの色味の違いを見てみましょう。

HDRディスプレイで表示した結果をSDRのPNG画像に落としたものです。
ゲージやその枠についてはどの手法でも同じ結果となっていることがわかります。

全体を見ると、Before IndirectとBefore Direct Rec2020では違いは少ないです。Directの方が少し明るいのは加算が入っているためでしょう。
Before IndirectではRec709/sRGBでオフスクリーンバッファに描画を行います。
この段階で1.0が値の上限になるわけですが、Directでは直接HDRのバッファに描画されるため1.0を超えることがあります。
After Direct Rec2020も同様ですが、加算合成のグローがかなり弱いです。

問題はAfter Indirectの加算合成部分です。かなり汚く出ていますね。
加算合成はかなり問題があると考えて良いでしょう。

もう1つの問題は明るさです。
SDRではトーンマップがかかった段階で明るさの上限が1.0となります。
この段階でブレンドする分にはどんなに明るい部分でも1.0なので、明るい部分に半透明ブレンドしてもブレンド率が低すぎなければ十分見えます。

しかしHDRではトーンマップをかけても1.0以上の値になります。
ここにちょっと明るい程度のUIをブレンドしても背景の明るさに負けてしまいます。
実際、ライティングをかなり明るくした場所にミニマップ的なものを描画すると以下のようになります。

SDRの方は円周部でも十分マップが見えますが、HDRでは円周部は消えてしまっています。
実際にHDRディスプレイで見るとより見づらいです。
ここからもわかると思いますが、不透明なら特に問題ないとはいえ、半透明はかなり問題になります。
加算合成が明るい部分で消えてしまうのはSDRでもHDRでも同じなので、重要な部分に加算合成は使うべきではありません。

また、今回のサンプルにはFSR/NISが含まれます。
FSRもNISもHDR対応はSDRと同じとは行きません。
FSRは色の範囲が0.0~1.0の範囲でしか対応していません。そのため、PQカーブをかけて1.0までの範囲内に収める必要があります。
NISはHDR対応しています。ただしPQカーブをかけた1.0の範囲か、1000nitsまでの明るさ(数値的には0.0~12.5)まで対応しています。
どちらを利用するかは NIS_HDR_MODE で指定します。今回はPQカーブで対応しています。

FSR/NISはUI描画の前に行いますが、HDRの場合はPQカーブを適用した値、つまりOETFを適用した結果を入力とします。
適用した状態にUIを描画してしまうと、常にAfter OETFへの描画になってしまい、Before OETFでの描画を行えません。
そのため、今回はBefore OETFを選択している場合に以下のような描画パスになります。

トーンマップの後にOETFを適用してからFSR/NISを適用します。
そしてEOTFで電気信号から光信号に再変換、UIを描画してからさらにOETF適用となります。
正直かなり面倒ですが、FSR/NISを使う場合は避けて通れないと考えたほうが良いでしょう。

参考文献

今回はHDRについてのプログラミング周りの解説は行っていませんし、色空間や伝達関数などの解説も行っていません。
これらの説明をするとぶっちゃけ長いし、自分でもよくわかってない部分があるので以下の参考資料を参照していただくと色々と理解しやすいのではないかと思います。