Unity で HDR (High Dynamic Range) と WCG (Wide Color Gamut) を考える (UnityHDROutputPlugin の解説)

## はじめに

次世代 (もう現世代ですが) の映像規格として 4K / HDR が標準化され、一般民生機器でも普通に HDR 対応している昨今、 Unity がいつまでたっても HDR 出力に対応してくれないので、 Editor 上で HDR 出力プレビューができるプラグインを作ってみました。

* [aosoft/UnityHDROutputPlugin](https://github.com/aosoft/UnityHDROutputPlugin)

![15.png](https://qiita-image-store.s3.amazonaws.com/0/211064/b43a5a0b-a686-393a-9ddc-b88926ff7111.png)

※このスクリーンショットは HDR ディスプレイでとってはいるのですが、JPEG/PNG では HDR にする事ができませんので高輝度部分は全部とんでしまっています。

本記事ではこのプラグインの目的と技術解説、及びそもそも "HDR とは？" というあたりから言及していきたいと思います。 HDR 関連の知識は私も曖昧なところもありますので、誤り等ありましたらご指摘いただけると幸いです。

HDR は "色" の理解が大事になってきます (私もまだまだ不十分ですが・・・) 。こちらの記事は大変勉強になりますので、是非ご一読ください。

* [XYZ色空間に迫る(1)](https://qiita.com/Ushio/items/203f16ad1e23fd42231c)
* [XYZ色空間に迫る(2)](https://qiita.com/Ushio/items/7a5d80612796c4bcfca6)


## HDR とは

HDR は "High Dynamic Range" の略 (最近の Unity で頻出する HDRP は "High Definition Render Pipeline" (高精度レンダリングパイプライン) なので、ここでの論点とは直接的には関係ありません) で、色の明るさの幅 (最も暗い～最も明るい) が従来より広いものを総称して呼ぶものです。

HDR に対して従来の仕様のものを SDR (Standard Dynamic Range) と呼びます。 SDR と HDR の境界は曖昧な感じがしますが、 SDR は 100nits (nits は明るさの単位で cd/㎡ と同じ値) くらいがピークになりますが、 HDR は 1000nits 以上くらいが表現可能であるかと思います。

### HDR レンダリング

Unity でいうところの HDR は今のところ "HDR レンダリング" のことを指します。

[Unity で HDR レンダリング](https://docs.unity3d.com/ja/current/Manual/HDR.html)

HDR レンダリング、という技術はものすごくざっくり言うと SDR を越えた範囲の色空間で演算 (レンダリング) を行い、最後にディスプレイで表現できる範囲にマッピングして表示する、というものです。人間の目で見る事ができる色、輝度は実際は相当広く、物理ベースレンダリングなどをしていて SDR の範囲外になったりするとその値はオーバーフロー／アンダーフローをしてしまいデーターが飛んでしまいます。最終的に SDR に押し込めるとしても中間レンダリングではオーバーフローしないデーターを維持することが重要なわけです。物理レンダリングとしての正しさを求めないとしてもオーバーフローによる演算結果の損失を防ぐ意図で行うこともあるかと思います。

### HDR 出力

"HDR 出力" は HDR に対応したディスプレイに HDR カラーの映像データーを出力して HDR のままの映像を表示することです。従来の HDR レンダリングでは最終的には SDR カラーにグレーディングして SDR ディスプレイに出力していたので本当の意味で HDR の映像が見れていたわけではありませんが、出力まで HDR にすることで HDR カラーを視認することが可能になるわけです。

HDR 出力は映像機器用に関連する規格が 2015 年頃から制定されており、近年 4K テレビ、ディスプレイが規格に準拠した HDR に対応してきています。出力側も PS4 Pro, Xbox One X といったゲーム機がサポートしていますし、特に大きいと思うのは **Windows 10 がデスクトップ環境での HDR 出力に対応** したことだと思います。これにより各々のアプリケーションで普通に HDR 表示に対応することが可能になり HDR 対応への敷居が一気に下がりました。

ただ、現状では Unity が HDR 出力までは対応していません。 Feedback には要望が上がっています。

* [HDR monitor support](https://feedback.unity3d.com/suggestions/hdr-monitor-support)



## 色空間

映像、というか色の定義は規格化されています。

ピクセル値は一般的に RGB で指定しますが、 **ここで指定する有効値はチャンネル毎に 0.0～1.0** (そうじゃない場合もありますが、多くの場合は) になります。この数値が実際にどのような色になるかは **どの規格 (色空間) で扱うか** で決定されます。現在は大抵の PC ではディスプレイの設定を "sRGB" にしていると思いますが、そのディスプレイでカラー値を決定した場合は sRGB 上での色になります。同じ数値でも違う色空間に持ってくると違う色になります。

sRGB などの色空間 (絶対色空間) は主に "色域 (color gamut)" と "伝達特性 (transfer characteristics)" から定義されます。

ちなみに Unity にも列挙型の定義だけは存在するのですが、今のところ有効に活用はされていなさそうです。 (Mac 向けビルドで Display P3 が選択できるのでそれくらい？)

* [ColorGamut](https://docs.unity3d.com/ScriptReference/ColorGamut.html)

![1.png](https://qiita-image-store.s3.amazonaws.com/0/211064/c175edef-dbd6-977e-f96d-07bf5d17ae18.png)

### 色域

色域は "表現できる色の範囲" を表します。人間の見える色は "CIE xy 色度図" というよく見る有名な図で表されている範囲が全てらしいですが、 sRGB といった規格はこの中の一部分を表しています。

![18.png](https://qiita-image-store.s3.amazonaws.com/0/211064/b24709c5-36cb-c6fb-9fef-afb93e273230.png)

図は [Colour](https://www.colour-science.org/) という Python スクリプトで作成させてもらいました (ものすごく強力です) 。グラデーションかかってる馬蹄型の部分が見える色域で、三角で囲われている部分が sRGB (BT.709) の範囲です。大分狭いです。

後にディスプレイ (テレビ) が 4K 化するのと合わせて BT.2020 という広色域 (Wide Color Gamut) な規格が策定され、 HDR は実態としては広色域でもあることになっています。

### 伝達特性 (転送特性)

明るさの数値をリニアに扱うのは計算上では都合がよいのですが、人間の目は明るい部分より暗い部分の方が区別しやすいので明るい部分より暗い部分に細かい階調を割り当てるようにした方が効率がよくなります。そこでリニア値に対して特定の関数 (伝達関数) を用いて必要に応じた変換を行います。

一般的に "ガンマ" と呼ばれているものですが、必ずしもガンマ関数とは限らないので "伝達特性" と呼びます。

### 代表的な規格

| Range | 名前 | 色域 | 伝達特性 |
| --- | --- | --- | --- |
| SDR | BT.709 | BT.709 | BT.709|
|| sRGB | BT.709 | sRGB |
|| Display P3| DCI-P3 | sRGB |
|| BT.2020 | BT.2020 | BT.709 |
| HDR | BT.2100 PQ | BT.2020 | PQ |
|| BT.2100 HLG | BT.2020 | HLG |

* BT.709 はテレビやビデオファイルで使われているもので、 sRGB は PC ディスプレイで主に使われているものです。
* Display P3 は DCI-P3 という映画向けの規格の色域に sRGB の伝達特性を組み合わせたもの (DCI-P3 は映画向けなのでピーク輝度が低い) 。


## Unity で HDR な映像制作をしていく

ここからは UnityHDROutputPlugin を使って HDR 映像の確認をしていきます。

### UnityHDROutputPlugin とは

Unity Editor 上でリニア色空間の RenderTexture を Windows の API を使って HDR (BT.2100 PQ) ディスプレイ上に HDR カラーでプレビューするものです。

先にも書きましたが、 Unity では現在最終出力は SDR にしか対応していません (Post Processint Stack のカラーグレーディングも SDR で綺麗に見えるように最適化されている) ので、 HDR でレンダリングしていても SDR 範囲外の色を視認することはできません。確認するには OpenEXR で静止画保存をし、何かしらの HDR プレビュー対応ソフトで見る必要がありましたが正直相当面倒なわけです。このプラグインを使えば実行中のリアルタイムな映像を確認できるので非常に効率がよいと思います。

ビルドしたアプリケーションには使えないのですが、 Editor での編集作業で

* 中間処理としての HDR カラーの確認
* HDR 動画作成のためのソース作り

あたりに使えるのではないかと考えています。

### UnityHDROutputPlugin の導入

[ドキュメント](https://github.com/aosoft/UnityHDROutputPlugin/blob/master/README.ja.md) を参照してください。

### HDR 効果を確認する

実際にプラグインを使ってみます。ここは HDR ディスプレイ環境がある事が前提になりますが、ある程度は SDR 環境でも確認できます。また、スクリーンショットは全て SDR です。

まず HDRP テンプレートでプロジェクトを作ります。

次に適当な RenderTexture (フォーマットは ARGB Float か ARGB Half にする。サイズは任意で) を作成し、 Main Camera の Target Texture にセットします。

プラグインの準備をします。 unitypackage を import し、メニュー "Window - HDR Display Output" でプラグインの設定ウィンドウを表示します。次に Source Texture に先に作成した RenderTexture をセットします。

![7_.png](https://qiita-image-store.s3.amazonaws.com/0/211064/3fdf0269-cc74-b753-3a5c-87da0c23e4b2.png)

"Open Window" を押すとプレビューウィンドウが表示されますが、実行中じゃないとプロジェクトの設定を変更しても描画に反映されないことがあるようです (RenderTexture を通しているから？) 。実行状態でプレビューするには先に Play ボタンを押して実行状態にしてから "Open Window" ボタンを押してください (プレビューウィンドウを先に開いていた場合、実行開始時に一旦閉じてしまうため) 。では表示を見てみましょう。

![8.png](https://qiita-image-store.s3.amazonaws.com/0/211064/14974210-60e5-a043-e4be-2eef75f5f608.png)

なんか暗いですね。これは Linear ガンマのデーターをそのまま描画しているためです。 "Convert Color Space" にチェックを入れてみてください。 sRGB 変換が入って通常の Game View の見栄えと同じになったと思います。

![9.png](https://qiita-image-store.s3.amazonaws.com/0/211064/677df42e-d7bb-db24-968a-3b0d32044ad6.png)

初期状態では Post Processing Stack により SDR に最適化された映像が生成されています。これだと途中が HDR で処理されていても出てくる映像データーは SDR になっていてプラグインの意味がありません。なのでカラーグレーディングを無効化しましょう。 "Hierarchy" の "Post-process Volume" を選択し、 Inspector の "Color Grading" 自体を無効化するか Tonemapping の Mode を "None" に変更してください。

![10_.png](https://qiita-image-store.s3.amazonaws.com/0/211064/6b783171-b182-0366-59d0-99c43f4ddcee.png)

するとものすごく白飛びした映像になりました。光の強さで柱の輪郭がほとんど見えなくなっているところがあります。 R, G, B の全チャンネルが 1.0 を越えてしまっているためです。トーンマッピングを切る前と比較すると PPS のカラーグレーディングはうまく処理をしているのもよくわかります。

![11.png](https://qiita-image-store.s3.amazonaws.com/0/211064/a8512ec3-0dec-dd26-7369-900b00abb906.png)

白いところは R, G, B の全チャンネルが 1.0 を越えているためですが、 **値が (1.0, 1.0, 1.0) でクリッピングされているわけではありません。** カラーグレーディングがされていないため素のデーターがそのまま来ています。 "Relative EV" の値を下げていくと光量が下がっていくので白飛びしている部分の色や階調が視認できるようになります。

![12.png](https://qiita-image-store.s3.amazonaws.com/0/211064/55458211-e47f-60c4-599e-1ecb5d4761d4.png)

![13.png](https://qiita-image-store.s3.amazonaws.com/0/211064/615d6b96-174c-9285-ea2a-aaebdff91bc1.png)


では "Relative EV" を 0 に戻し、 "Request HDR Output" にチェックを入れてみてください。 HDR 出力に対応している環境で見るとこれまでと大きく異なる映像になるかと思います (実機じゃないと確認できないのでお見せできません) 。まず白にしか見えなかった大部分のところには白以外の色があることと、光源の中心の部分はかなり高輝度の白になっていると思います。このように同一の映像の中に大きな明暗の差を表示できるのが HDR ならではといったところでしょう。

### Unity の HDR レンダリング時のピクセル値を考える

Unity のデフォルトのカラーフォーマットは sRGB です。 sRGB では色域が BT.709 、伝達特性が sRGB (ガンマ 2.2) になります。
 Project Setting の Color Space を "Gamma" にしているとレンダリングも Gamma (= sRGB) になりますが、 Color Space を "Linear" にするとレンダリングは Linear で行います (ディスプレイ出力するバックバッファへのレンダリング時に sRGB への変換する) 。

ということで Color Space = Linear 時、内部レンダリングのカラーフォーマットは **"色域 = BT.709, 伝達特性 = Linear"** になります。

また、 RenderTexture のフォーマットを浮動小数点値系 (ARGB Float, ARGB Half) にすることで **0.0～1.0 の範囲外の値もそのまま保持できる** 事がポイントです。というのも 0.0～1.0 の範囲では sRGB の範囲の色、明るさしか表現できません。範囲外をクリッピングしてしまうと色情報が欠損してしまいます。クリッピングしない事で SDR のフォーマットでも HDR な色が表現できるわけです。

#### HDR カラー (より明るい色)

まず BT.709 の色域の範囲でより明るい色を指定しましょう。

1. HDR カラーピッカーを開き、 "RGB 0.0-1.0" にします。
2. R, G, B を 0.0-1.0 の範囲で色を決めます。
3. Intencity で明るさを調整します。

ここでの重要なポイントは **明るさの調整で R, G, B の各チャンネル値を直接操作しないこと** です。 sRGB の色は Unity Editor 上で視認できる (はず) なので、まず sRGB 範囲で色を確認した後に Intensity で明るさを調整する、というのがよいかと思います。

#### WCG カラー (より広色域)

先ほどは各チャンネルの値をバラバラに 1.0 をこえるような調整をしない、としましたが、これを越えると色域をはみ出すので **BT.709 に対して広色域** になります。

ではどのような値を指定しても広色域になるといえるのでしょうか？

明るさを変える (R, G, B 各チャンネルに同じ係数をかける) ことでは色域は変わりません。よって単純に 1.0 をこえる値を指定すれば色域をはみだす、ということではありません。色域を決めるパラメーターに明るさはありません。ので HDR ＝ 広色域ではありません。色域は R, G, B の各チャンネル値のバランスで決定します。

![17.png](https://qiita-image-store.s3.amazonaws.com/0/211064/d119e440-d558-53f9-a794-503e79642e7a.png)


先ほどの図に BT.2020 の範囲をオレンジ色の三角で合わせました (BT.709 は青) 。BT709 と BT.2020 を見比べると BT.2020 は BT.709 を内方しているので BT.2020 は BT.709 を全て表現できます。しかし当然逆は無理なわけです。ですが 0.0～1.0 の範囲にこだわらなければ BT.709 でも広色域を表現可能です。ただ **BT.709 で BT.2020 を表現するには負の値を許容する必要があります。**

これは現在の Unity ではちょっと扱うのは難しい (Unity のカラーピッカーでは設定できない) し、今のところは考えない方がよいでしょう。現時点ではディスプレイ側が BT.2020 に対応しているといっても実際に表示できる色域はそこまで広くないと思いますし。

ちなみに sRGB の拡張版の [scRGB](https://en.wikipedia.org/wiki/ScRGB) では基本的な定義を sRGB と同じにしながら値の範囲を -0.5～7.5 とすることで sRGB (BT.709) との互換性を持ちながら色域 (とダイナミックレンジも) を広げたようです。

## UnityHDROutputPlugin の技術解説

### Direct3D の HDR 出力対応

HDR 出力の実装は [Microsoft/DirectX-Graphics-Samples](https://github.com/Microsoft/DirectX-Graphics-Samples) の [HDR sample (D3D12HDR)](https://github.com/Microsoft/DirectX-Graphics-Samples/tree/master/Samples/Desktop/D3D12HDR) を参考にしています。このサンプルは Direct3D 12 で実装されていますが、 HDR 機能は DXGI の API で提供されているので Direct3D 11 からでも 12 の API は一切使用せずに HDR 出力は実装可能です (本プラグインも 11 のみで実装しています) 。

下記も参考になります。

* [HDR Ecosystem for Games](https://developer.download.nvidia.com/assets/gameworks/downloads/secure/GDC18_Slides/HDR_GDC2018.pdf?sUQ_d7wa5inwiuapDPm5s2TSsSQXNeZezGdFPZkAvZ4fUDVq5t9qhPtYvFDom0qnNnWbrRmL6MFTK4E4werBx3M-WLD1NtCW0UuevjFzTAl6u2CDus37_KZbVfT_s9Tas3ycoEF_Zl7GxRO7Lo65JJUxdei0VwtQhKceEMQ) - HDR の概要から実装面まで網羅的に書かれており、実装はしなくても一読の価値あり
* [Lighting up HDR and advanced color in DirectX](https://channel9.msdn.com/events/Build/2017/P4061)

ざっくり要点としては

* DXGI の SwapChain を HDR 対応できるように設定する。
* SwapChain の BackBuffer に書き込む際、 BackBuffer に設定をした色空間に変換して書き込む。

### Unity Plugin として

先に書いた通り、 HDR の設定は SwapChain に行う必要がありますが、 SwapChain は Device 等から逆引きに引きずり出すことができないので自前で作成するしかありません。そのため Preview Window は Unity とは無関係に自前で作成をしています。また、 Unity の Window を HDR にできないのでビルドしたアプリ用ではなく Editor 専用としています。

当初は Unity が使用している Device そのものを直接使ってレンダリングをしていたのですが、非常に不安定で原因がよくわからなかったので、 Preview Window は独自 Device 、独立スレッドで処理をするようにしました。この関係で Unity 側のレンダリングより 1 フレーム以上は遅延していると思いますが、 Editor 専用で描画の品質を確認するためのものでパフォーマンスはそこまで重視しないという事で割り切っています。

Unity 側でレンダリングされた Textrure (RenderTexture) のデーターは Preview Window 用 Device に転送する必要があります。ここでは DXGI の共有リソースという機能を使って GPU 内だけで転送が完結するようにしています。コピーの仕方としては最速ですが、そもそもコピーが必要になってしまっているところが惜しいところです (Unity 側の Device でレンダリングしていたら不要だった) 。

余談ですが、 Unity が使用しているの Device (DeviceContext) を適当なタイミング (Update とか) で使うと描画が破綻してなかなか面白いことになります。処理内容によりますが、原則的に UnityRenderingEvent 等を使わないとダメです。

## おわりに

HDR 出力プラグインについての解説と関連技術についての解説をしてみました。

Unity が現時点で対応していないのは、おそらく HDR 出力そのものの対応よりレンダリングパイプラインやアプリ開発時のワークフローの HDR 対応に課題があるのではないかなあと個人的には思います (内部レンダリングの色空間をどうするのか、 Post Processig Stack の対応、 UI (カラーピッカーなど) 、 HDR の静止画や動画のインポートなど) 。"Windows 10 で HDR 出力をする" だけだったら技術的には難しくないはずなので。

HDR 表示はデーター送出側 (GPU, OS) も表示側 (ディスプレイ、テレビ) も十分一般レベルに落ちてきていて普及は時間の問題だと思うので Unity には本当に早いところ対応をしてほしいところです。