UE4のHDR対応はどうなってるの？Win10編

# はじめに
　この記事は[Unreal Engine (UE) Advent Calendar 2021](https://qiita.com/advent-calendar/2021/ue)その１の14日目の記事です。
　クリスマスまでにUE4のダイナミックレンジを上げていきたい人に注目の記事です。
　昨今はやりのHDRですが、実際にHDRってなに？ってところは詳しくやりません。参考文献を最後に挙げてありますのでそれを読んでいただいたり、いろいろ検索すれば出てくるのでそちらを読んでください。HDRに関しては僕自身わからないことだらけで、この記事にも適当なことを書いてると思います。話半分で参考にしてください。特に色空間の話になってくるとまだまだ勉強不足です。参考文献の一番目のポリフォニー・デジタル内村さんのスライドに必要なことはだいたい書いてありますのでぜひ目を通してください。

　この記事ではWin10+DirectX 12でUE4のHDRがどうなってるかを調べた結果をご紹介します。

# HDRってなに？
　詳しくやらないと言いつつちょっとだけ解説。
　HDR = High Dynamic Rangeで色や輝度のレンジが広いことと非常に大雑把に定義します。他にもカラースペースやらガンマやら細かい事色々あって書ききれないし、筆者がちゃんと理解ができてない事が多いので。
　とは言っても実は昨今のゲームレンダリングはほとんどがHDRで行われています。UE4もライティングやポストプロセスの多くはHDR領域で行われます。普段はあまり意識していないかもしれませんが、日常的にHDRでレンダリングしているわけです。しかし、HDRでレンダリングしても出力先のテレビやディスプレイがSDRでしか表示できないので、トーンマップを行うことでHDR画像をSDRにダイナミックレンジを縮小して表示しています。
　ここ数年でかなり普及しはじめたHDRテレビやHDRディスプレイはいままでダイナミックレンジを縮小して表示していたレンダリング結果を***あまり縮小せずに***表示できるようになりました。
　この***あまり縮小せずに***というところが味噌というか厄介なところで。レンダリング結果をそのまま出力すれば良いというそこまで簡単な話では無かったりします。
　HDRをちゃんと語るには素材の色空間、ディスプレイの校正、最大輝度最小輝度の考慮などなど切りがないのですが、その辺は（自信もないし）やりません。

# UE4とHDR

## 準備

### Windows10とディスプレイ
　HDRをやるにはHDR対応のディスプレイが必要です。対応のディスプレイを持っていない人はサンタさんにお願いしてください。今からでも間に合うかもしれません。UE4のバージョンは4.27.1を使用しています。

　まずはディスプレイのモードをHDRに切り替えましょう。Windowsのディスプレイメニューで**HDRを使用する**をオンにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/4e0e0c63-2e30-5392-80ec-8511d8054256.png)
　オンにしたら**Windows HD Color 設定**をクリックしてHDR/SDR 明るさのバランスは最低レベルにしてみましょう。これをすると画面が暗くて見づらくなるかもしれませんがこの画面内のHDR感がわかりやすくなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/5128350d-6f1d-595e-b4d0-e2fc5553c7f9.png)
　正しくHDR表示できている場合は左の画像の太陽が明るく、逆光になった人物は黒く、「HDR感」が確認できる表示になっていることと思います。
　このHDR/SDR明るさのバランスですが、実はHDR描画されている部分には影響しないみたいですが、SDRとHDRが混在している場合にSDR側の輝度が高いために相対的にHDR画像の見え方が変わってしまうように思えます。人間の脳の問題なのかなと。ディスプレイによってはローカルディミングの影響もあるかもしれません。
　試しにYoutubeのHDR動画を表示しながらバランスを動かしてみると動画内の輝度は変わらないけどそれ以外の部分は明るくなったり暗くなったりするかと思います。

:::note info
手っ取り早くHDRを体験するなら、YoutubeでHDRで検索してみましょう。HDR対応動画が色々あります。良いHDR動画とそこそこのHDRディスプレイがあると、SDRとは段違いに美しい色とコントラストの動画が再生できます。
:::

### UE4のHDR

　UE4でHDRで出力する方法に関しては、公式ドキュメント[High Dynamic Range 対応ディスプレイへの出力](https://docs.unrealengine.com/4.27/ja/RenderingAndGraphics/HDRDisplayOutput/)にまとめられています。
　コンソール変数のr.AllowHDRを1にして、r.HDR.EnableHEDROutputを1にするだけです。簡単ですね。**以上です！**

　・・・これで終了なら良かったのですが、まあ世の中そんなに甘くないということで、ここからが本番です。

　コンソールコマンドで `r.AlloHDR 1` と入力すると、`Error: r.AllowHDR is read only!`と怒られます。起動後に変更はできないということなので、DefaultEngine.iniなどに書いておきましょう。

```
[ConsoleVariables]
r.AllowHDR=1
```

:::note info
起動時のオプションで -hdr を指定することでもHDRを有効にすることができます。
:::

　また、RHIはDirectX 12にしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/6dc2328d-c45e-7d69-5805-0365b5f81730.png)
　DirectX 11でもHDR対応はされているみたいですが、この記事ではDirectX 12で検証します。

　そして、UE4エディタを起動して、`r.HDR.EnableHDROutput 1`を入力するとこんなログが出ます。

```
Cmd: r.HDR.EnableHDROutput 1
r.HDR.EnableHDROutput = "1"
LogD3D12RHI: Setting HDR meta data on swap chain (0x0002662f5c5f60) using DisplayGamut 2:
LogD3D12RHI:         MaxMasteringLuminance = 1000.0000 nits
LogD3D12RHI:         MinMasteringLuminance = 0.0000 nits
LogD3D12RHI:         MaxContentLightLevel = 0 nits
LogD3D12RHI:         MaxFrameAverageLightLevel 0 = nits
LogD3D12RHI: Setting color space on swap chain (0x0002662f5c5f60): RGB_FULL_G10_NONE_P709
```
何やら切り替わっているようです。他のパラメーターがどうなっているか、コンソールコマンドで確認してみます。

```
Cmd: r.HDR.Display.OutputDevice
r.HDR.Display.OutputDevice = "5"      LastSetBy: DeviceProfile
Cmd: r.HDR.Display.ColorGamut
r.HDR.Display.ColorGamut = "2"      LastSetBy: DeviceProfile
```

ACES 1000-nit ScRGB (HDR)
Rec2020 / BT2020, D65
で表示されているようです。

この状態でエディタ画面を見ると、UIの色がちょっと褪せたグレーになっていて若干見にくい状況です。
確認用に黄色いポイントライトをテストレベルに置いて最大輝度の160.0 cdにしてみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/f8be5fa9-c07e-8ac7-3ae8-2fa4ce2d5dfd.png)
「HDR状態（と思われる）ものをスクリーンキャプチャして貼り付けたってそれはHDRじゃないやろ」と当然の意見になるのですが、以外と見た目は近い感じに表示されています。

　黄色いライトの中心あたりが白くなっていますが、これは明るすぎるライトが飽和している状態です。つまり1.0以上の明るさになった色は1.0にクリップされてしまうので、RGB全部1.0になり白になってしまいます。ということはHDRになっていないと思われます。
　HDRでも表現できる明るさには限界があるので、極端に明るいと白に飽和してしまいますが、この程度では飽和しないはず。

　公式ドキュメントには
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/ea0641aa-7c9f-94ae-b8c2-364fabcfbe3a.png)
とあります。

　そこでスタンドアローンで起動してみます。
　起動後、`r.HDR.EnableHDROutput 1`を実行すると、中心が完全に白にはならず、黄色の発色も綺麗なおそらくHDR状態で表示されます。しかもウィンドウモードの状態でHDRに切り替わっているようです。
　とはいえ、その状態をSDRのディスプレイでお見せすることはできないのでそこがHDRの厄介なところです。「これがHDR」を伝えるのが難しい。普通にスクリーンキャプチャしてもSDR状態でキャプチャされてしまいます。

　そこで、Windows Xbox Game Barの登場です。Windowsキー+Gで起動し、HDR状態をキャプチャすることができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/c58bd74d-62fb-a0ce-18ca-bd5971d8c9ca.png)

　Xbox Game BarでHDR画面をキャプチャすると２つのファイルが生成されます。左がHDR画像、右がSDR画像です。一見反対に見えますが、左はHDRで格納されたJPEG XR画像をSDRで見ているので明るい部分が白にクリップされているのだと思います。右はHDR画像をXbox Game BarがSDR領域にトーンマップしてpngで保存したものだと思われます。トーンマップの結果黄色いライトがクリップされない状態を見ることができます。その反面、通常のSDR領域は相対的に暗くなります。？アイコンの色などで差がよくわかると思います。
　つまりは左はHDR画像のSDR領域を見ている状態、右はHDR画像をSDRにマッピングした状態という事になります。どちらもHDR画像そのものでは無いですが、雰囲気はつかめるのでは？SDR画像ではありますが、右の画像のように見えて欲しいわけです。なお、左の画像をXbox Game Barのギャラリーで見ると、キャプチャ時と同様のHDRで見ることができます。

　ということで、なんとなくHDR出力はできていそうな気配。しかも、エディタ上ではダメだけど、スタンドアローン実行ではフルスクリーンにしなくてもHDRになっていそうに見えます。

:::note info
Win10がHDRに対応し始めた頃は排他フルスクリーンでないとHDRにならなかったみたいですが、その後のアップデートで現在はWindowごとにHDR表示を設定できるみたいです。
:::

## 検証編

　それではRenderDocでキャプチャして実際にバッファがどうなってるか見てみたいと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/1b70424c-5460-80cc-5e86-a0fc17325958.png)
　ライティングパスが終わったところで黄色いライトの中心あたりの床のRGB値を見ると、(33.875, 33.875, 0.20483)という、赤と緑が非常に強い値になっているのがわかります。この状態はSDR出力だろうがHDR出力だろうが変わりません。SDR出力の場合はこの値が1.0を超えないようにトーンマッピングされます。

　ではHDRの場合のトーンマップ後のバッファを見てみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/8f9fdba7-6c37-804c-059b-eee274899860.png)
　中心付近の明るい部分が(12.64844,11.57031,2.99609)という数値になっています。SDRでキャプチャした画像を見ているので白飛びしていますが、HDR出力で見ると一番明るい部分でも真っ白にはなっていません。

　比較のために同じ状況でSDR出力の場合のトーンマップ後のバッファを見ると
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/b203eac5-7fce-bd08-e5a7-706f46407363.png)
　(1.00,1.00,0.95601)と1を超えない値に抑えられています。どんなに明るい部分でも1.00を超える値にはならないようにマッピングされているのがわかります。厳密には画面の起動を調べて一番明るいところが1を超えないようにトーンマッピング処理をしてバッファに出力しますが、バッファ自体も1を超える値はクリップされるので、どうやっても1を超える事はありません。

　ここまででわかった事としては、HDR時とSDR時でどちらもトーンマップは行われているが、1.00以下にマップするSDRと違って、HDRではある程度大きな値までの範囲にマップされています。HDRだからといってトーンマップは不要ということは無さそうです。
　HDRでは概ね12.5が最大になるようにマップしているように見えますが、たまに13くらいになっているところもあります。Microsoftの[高ダイナミックレンジ表示および高度な色でのDirectXの使用](https://docs.microsoft.com/ja-jp/windows/win32/direct3darticles/high-dynamic-range)という資料を見ると
`たとえば、scRGB (1.0, 1.0, 1.0) は、標準の D65 ホワイトを 80 nits にエンコードします。しかし、scRGB (12.5, 12.5, 12.5) は、同じ D65 ホワイトをより明るい 1000 nits でエンコードします。`
と書かれていますが、この12.5と関係あるのでしょうか？最も明るいところがだいたい1000 nitsになるようにマップしている？

　UE4の内部パラメーターを表示してみると
SDR
r.HDR.Display.OutputDevice = 0 : sRGB(LDR)
r.HDR.Display.ColorGamut = 0 : Rec709 / sRGB, D65

HDR
r.HDR.Display.OutputDevice = 5 : ACES 1000-nit ScRGB(HDR)
r.HDR.Display.ColorGamut = 2 : Rec2020 / BT2020.D65

　SDR時はsRGBでRec709、HDR時はScRGBでRec2020/BT2020で出力されているのがわかります。細かくは説明しませんが、Rec709は今までのSDRディスプレイの色空間、Rec2020はHDRに対応したより広い色空間です。

:::note info
SDRとLDRは同じものと思って良いかと思います。
Standard Dynamic RangeとLow Dynamic Rangeの略。
:::

　UE4にはHDR状態を表示するモードがあります。
`ShowFlag.VisualizeHDR`
　と入力するとヒストグラムと中心部分の輝度が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/fd104fcf-d339-5f32-3037-debc7ceb21fe.png)
　グレイマンの光の当たった部分は600 nitくらいの明るさになっていることが確認できます。

　今度はビルドを作成して起動してみます。と、ここで問題が発生しました。ゲームビルドで`r.HDR.EnableHDROutput 1`を入力してもHDRになりません。色の感じは変わるのですが、明るいところが飽和して白になってしまいます。
　試しにエディタに -game オプションを付けて実行した場合はHDRになってくれますが、ゲームビルドはダメです。これは困りました。

:::note info
エディタからのスタンドアローン実行や、エディタの-gameオプション起動はあくまでもゲームビルドとほぼ同じ動作をするというモードなので、時に動作の違いがあります。
内部的にはWITH_EDITOR、WITH_EDITORONLY_DATAといったdefineが有効な状態で実行されています。
特にパッケージビルドで無いためにローディング時間が長かったり非同期ロードが機能しなかったり、各種チェックやデバッグ機能のためにパフォーマンスが低かったり。
ゲームビルドでの動作確認を怠らないようにしましょう。
:::

　ゲームビルドでもVisualizeHDRは有効なので使ってみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/49536aad-9c52-7e06-7a70-86b7ea9d6ae7.png)
　十分明るい数値は出てますが、まあこれはトーンマップ前のバッファなので当然かな。最終画像は明るい部分が白にクリップされています。どうやらトーンマップが怪しい？

:::note info
VisualizeHDRの数値表示が白だと明るいところで潰れて数字が見えないので色を変更してます。
Shaders/Private/PostProcessVisualizeHDR.usfのPrintFloatなどのFontColorを変更。
:::

　トーンマップに問題があるならとりあえずトーンマップをスキップしてみよう。ということで、何もしないトーンマップマテリアルを作ってPostprocessVolumeにセットしてみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/5e573728-7963-65b9-7519-6ce61280174f.png)
　結果はやはりこのポストプロセスの結果が1.0にクリップされていました。トーンマップが原因では無く、どうやら出力バッファの問題な気がしてきました。

## 解析編
　ゲームビルドがHDR描画にならないのでUE4内部の解析に入ります。DirectX12での調査です。

　まず、ディスプレイがHDRに対応しているかどうかを調べているのが

```WindowsD3d12Device.cpp
static bool SupportsHDROutput(FD3D12DynamicRHI* D3DRHI)
{
	// Determines if any displays support HDR
	check(D3DRHI && D3DRHI->GetNumAdapters() >= 1);

	bool bSupportsHDROutput = false;
	const int32 NumAdapters = D3DRHI->GetNumAdapters();
	for (int32 AdapterIndex = 0; AdapterIndex < NumAdapters; ++AdapterIndex)
	{
		FD3D12Adapter& Adapter = D3DRHI->GetAdapter(AdapterIndex);
		IDXGIAdapter* DXGIAdapter = Adapter.GetAdapter();

		for (uint32 DisplayIndex = 0; true; ++DisplayIndex)
		{
			TRefCountPtr<IDXGIOutput> DXGIOutput;
			if (S_OK != DXGIAdapter->EnumOutputs(DisplayIndex, DXGIOutput.GetInitReference()))
			{
				break;
			}

			TRefCountPtr<IDXGIOutput6> Output6;
			if (SUCCEEDED(DXGIOutput->QueryInterface(IID_PPV_ARGS(Output6.GetInitReference()))))
			{
				DXGI_OUTPUT_DESC1 OutputDesc;
				VERIFYD3D12RESULT(Output6->GetDesc1(&OutputDesc));

				// Check for HDR support on the display.
				const bool bDisplaySupportsHDROutput = (OutputDesc.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020);
				if (bDisplaySupportsHDROutput)
				{
					UE_LOG(LogD3D12RHI, Log, TEXT("HDR output is supported on adapter %i, display %u:"), AdapterIndex, DisplayIndex);
					UE_LOG(LogD3D12RHI, Log, TEXT("\t\tMinLuminance = %f"), OutputDesc.MinLuminance);
					UE_LOG(LogD3D12RHI, Log, TEXT("\t\tMaxLuminance = %f"), OutputDesc.MaxLuminance);
					UE_LOG(LogD3D12RHI, Log, TEXT("\t\tMaxFullFrameLuminance = %f"), OutputDesc.MaxFullFrameLuminance);

					bSupportsHDROutput = true;
				}
			}
		}
	}

	return bSupportsHDROutput;
}
```
　ディスプレイのカラースペースがBT2020に対応しているかどうかで判定しているようです。
　HDRに設定するのは

```WindowsD3D12Viewport.cpp
void FD3D12Viewport::EnableHDR()
{
	if ( GRHISupportsHDROutput && IsHDREnabled() )
	{
		static const auto CVarHDROutputDevice = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.HDR.Display.OutputDevice"));
		const EDisplayFormat OutputDevice = EDisplayFormat(CVarHDROutputDevice->GetValueOnAnyThread());

		const float DisplayMaxOutputNits = (OutputDevice == DF_ACES2000_ST_2084 || OutputDevice == DF_ACES2000_ScRGB) ? 2000.f : 1000.f;
		const float DisplayMinOutputNits = 0.0f;	// Min output of the display
		const float DisplayMaxCLL = 0.0f;			// Max content light level in lumens (0.0 == unknown)
		const float DisplayFALL = 0.0f;				// Frame average light level (0.0 == unknown)

		// Ideally we can avoid setting TV meta data and instead the engine can do tone mapping based on the
		// actual current display properties (display mapping).
		static const auto CVarHDRColorGamut = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.HDR.Display.ColorGamut"));
		const EDisplayGamut DisplayGamut = EDisplayGamut(CVarHDRColorGamut->GetValueOnAnyThread());
		SetHDRTVMode(true,
			DisplayGamut,
			DisplayMaxOutputNits,
			DisplayMinOutputNits,
			DisplayMaxCLL,
			DisplayFALL);

		// Ensure we have the correct color space set.
		EnsureColorSpace(DisplayGamut, OutputDevice);
	}
}
```
　デバッガで追いながら実行すると、HDRの設定は正常に行われているようでした。
　しかし最終的な描画は1.00にクリップされてしまっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/24ea2cab-0fd4-37db-3ce2-ee1f28b94c38.png)

　どうやらここが問題ぽいです。

```WindowsD3D12Viewport.cpp
void FD3D12Viewport::EnsureColorSpace(EDisplayGamut DisplayGamut, EDisplayFormat OutputDevice)
{
	ensure(SwapChain4.GetReference());

	DXGI_COLOR_SPACE_TYPE NewColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709;	// sRGB;
	const bool bPrimaries2020 = (DisplayGamut == DG_Rec2020);

	// See console variable r.HDR.Display.OutputDevice.
	switch (OutputDevice)
	{
		// Gamma 2.2
	case DF_sRGB:
	case DF_Rec709:
		NewColorSpace = bPrimaries2020 ? DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P2020 : DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709;
		break;

		// Gamma ST.2084
	case DF_ACES1000_ST_2084:
	case DF_ACES2000_ST_2084:
		NewColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020;
		break;

		// Gamma 1.0 (Linear)
	case DF_ACES1000_ScRGB:
	case DF_ACES2000_ScRGB:
		// Linear. Still supports expanded color space with values >1.0f and <0.0f.
		// The actual range is determined by the pixel format (e.g. a UNORM format can only ever have 0-1).
		NewColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709;
		break;
	}

	if (ColorSpace != NewColorSpace)
	{
		uint32 ColorSpaceSupport = 0;
		if (SUCCEEDED(SwapChain4->CheckColorSpaceSupport(NewColorSpace, &ColorSpaceSupport)) &&
			((ColorSpaceSupport & DXGI_SWAP_CHAIN_COLOR_SPACE_SUPPORT_FLAG_PRESENT) != 0))
		{
			VERIFYD3D12RESULT(SwapChain4->SetColorSpace1(NewColorSpace));
			UE_LOG(LogD3D12RHI, Log, TEXT("Setting color space on swap chain (%#016llx): %s"), SwapChain4.GetReference(), *GetDXGIColorSpaceString(NewColorSpace));
			ColorSpace = NewColorSpace;
		}
	}
}
```
　ゲームビルドではこのif文の中のコードが実行されません。新しいカラースペースをSwapChain、つまり画面出力用のバッファにセットしたいのだが、対応していないカラースペースなのでSetColorSpace1が実行されないという状況のようです。
　RenderDocでBackBufferのフォーマットを見るとエディタでは16bitバッファなのですが、ビルドでは10bitバッファになっているので、それが原因なのかな？HDR10は10bitと聞いてたんだけど・・・

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/3c5d6ef8-090b-2734-7ee8-6eaccc1d223c.png)
　プロジェクト設定でフレームバッファをFloatにしてみたけど駄目でした。この設定では無さそう。

　出力バッファのフォーマットを設定しているところを探します。

```WidnowsD3D12Device.cpp
	{
		GRHISupportsHDROutput = SupportsHDROutput(this);

		// Specify the desired HDR pixel format.
		// Possible values are:
		//	1) PF_FloatRGBA - FP16 format that allows for linear gamma. This is the current engine default.
		//					r.HDR.Display.ColorGamut = 2 (Rec2020 / BT2020)
		//					r.HDR.Display.OutputDevice = 5 or 6 (ScRGB)
		//	2) PF_A2B10G10R10 - Save memory vs FP16 as well as allow for possible performance improvements 
		//						in fullscreen by avoiding format conversions.
		//					r.HDR.Display.ColorGamut = 2 (Rec2020 / BT2020)
		//					r.HDR.Display.OutputDevice = 3 or 4 (ST-2084)
#if WITH_EDITOR
		GRHIHDRDisplayOutputFormat = PF_FloatRGBA;
#else
		GRHIHDRDisplayOutputFormat = PF_A2B10G10R10;
#endif
	}
```
　プロジェクト設定にかかわらずHDR時のバックバッファのフォーマットはここでハードコードされていました。エディタではPF_FloatRGBAですが、ゲームビルドではPF_A2B10G10R10が設定されているみたいです。
　ここのコードを書き換えてゲームビルドもPF_FloatRGBAに変更したところ、ようやくゲームビルドもHDRになりました。つまりエディタと同じ状態になったようです。
　これで解決？と早合点する前にこの部分のコメントを良く読みましょう。

```
		//	2) PF_A2B10G10R10 - Save memory vs FP16 as well as allow for possible performance improvements 
		//						in fullscreen by avoiding format conversions.
		//					r.HDR.Display.ColorGamut = 2 (Rec2020 / BT2020)
		//					r.HDR.Display.OutputDevice = 3 or 4 (ST-2084)
```
　ここです。そうです。エディタ以外では10bitバッファなのでOutputDeviceは3か4を使えとちゃんと書いてあります。
　以下のMicrosoftのドキュメントにも書いてありました。
***
　[Microsoftのドキュメント](https://docs.microsoft.com/ja-jp/windows/win32/direct3darticles/high-dynamic-range)から抜粋
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/b9947d11-69b8-2f8d-b283-c65a0c152d17.png)
***

　ということでゲームビルドを起動後に以下のコマンドを入力します。

```
r.HDR.EnableHDROutput 1
r.HDR.Display.OutputDevice 3
```
　お、どうやらHDR描画になったようです！

　組み合わせをまとめると

|バッファフォーマット|カラースペース|出力デバイス|
|:------------------|:--------------|:-------------|
|PF_FloatRGBA       |DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709|ACES 1000-nit ScRGB (HDR)|
|PF_A2B10G10R10     |DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020|ACES 1000-nit ST-2084 (Dolby PQ) (HDR)|

　というのが正解のようです。つまり、エディタやスタンドアローン実行と、ゲームビルドでは出力デバイスの設定が違うということです。ゲームビルドはパフォーマンス優先なのとメモリ節約のために、32bitサイズのPF_A2B10G10R10を使用し、10bitでも高い輝度に対応できるようにPQカーブを適用するということでしょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/0cd9d216-8565-ed03-6691-3e10e13372d6.png)

　RenderDocでキャプチャしてみると、すごく明るい場所でも0.75くらいの数値です。PQカーブは非線形なので0.75で600nitくらいの明るさになるということでしょうか。

　検証のため画面上部にトーンマップ前のグラデーション、画面下部にトーンマップ後のグラデーションを表示し、エディタ実行とゲームビルド実行を比較してみました。上部のグラデーションも下部のグラデーションも左が０、右が４です。Xbox Game Barでキャプチャしたものなので、SDRにトーンマップされていますが、雰囲気はつかめるかな。

　エディタから実行、バッファフォーマットはPF_FloatRGBA、出力はScRGB
![HDRTest プレビュー [NetMode_Standalone]  (64-ビット_Windows)  2021_12_13 15_15_59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/931f95ba-d94a-c6ea-b4a9-f655c74cf96a.png)

　ゲームビルド実行、バッファフォーマットはPF_A2B10G10R10、出力はDolby PQ
![HDRTest (64-bit Development PCD3D_SM5)  2021_12_13 15_15_38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/d04a1b1c-6974-9c93-6895-7ffd1f7d8291.png)

　エディタ実行だと上部と下部のグラデーションがほぼ同じですが、ゲームビルド実行は下部のグラデーションが早い段階で飽和しています。つまりは1.0が最大値にマッピングされているが、出力はHDRになっていることが伺えます。つまり、Dolby PQは0-1.0の間にHDR領域、この場合は1000nitまでの明るさをマッピングしているのだと思います。上部のグラデーションは4でも白になりきってないので、まだまだ余力があるということですね。
　結果をまとめるとエディタではというか、16bitバッファでのHDRレンダリングでは1を超える数値を1を超えたまま出力しているが、ゲームビルドでの10bitバッファでのHDRレンダリングでは1を超える数値を0-1の範囲の非線形領域にマップして出力しているのがわかります。

# UE4エディタはHDRにならないの？

　どうせならUE4エディタでHDR状態で作業したいと思いますよね。しかし、現状はエディタ上で`r.HDR.EnableHDROutput 1`と入力してもHDRにはなりません。エディタ上でHDR描画できれば検証も調整も楽なのですが、対応されていないのでしょうか。RenderDocでエディタの描画を見ると、出力バッファのフォーマットがR16G16B16A16_UNORMになっています。これが原因でしょうか？

:::note info
RenderDocでSlateの描画を見るためにはプロジェクト設定でRenderDocを選択して、Capture all activityをチェックします。
:::

　エディタ上での描画はすべてSlateなのでSlate関連のソースコードを覗いてみます。

```SlateRHIRenderer.cpp
void FSlateRHIRenderer::CreateViewport(const TSharedRef<SWindow> Window)
{
// 中略
		if (IsHDREnabled())
		{
			NewInfo->PixelFormat = GRHIHDRDisplayOutputFormat;
		}
```
　あれ？対応してそうだけど？最初からHDRモードならいけるのかな？とiniファイルに指定してみました。

```DefaultEngine.ini
[ConsoleVariables]
r.AllowHDR=1
r.HDR.EnableHDROutput=1
```
　しかし、やはりエディタを起動してもHDRになりません。ソースコードをざっと見てみると

```SlateRHIRenderer.cpp
EPixelFormat FSlateRHIRenderer::GetSlateRecommendedColorFormat()
{
	return bIsStandaloneStereoOnlyDevice ? PF_R8G8B8A8 : PF_B8G8R8A8;
}
```
　ここで8bitバッファに設定されてしまってますね。ちょっと書き換えてみましょう。

```SlateRHIRenderer.cpp
EPixelFormat FSlateRHIRenderer::GetSlateRecommendedColorFormat()
{
#if PLATFORM_WINDOWS
	if (IsHDREnabled())
	{
		return GRHIHDRDisplayOutputFormat;
	}
#endif
	return bIsStandaloneStereoOnlyDevice ? PF_R8G8B8A8 : PF_B8G8R8A8;
}
```
　こんなふうに修正してみると・・・

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/23efbf43-d9f5-649d-3f7b-525c5393f0fd.png)
　HDRになりましたね。ってSDRでスクショ貼ってもわからないですが、試してみてください。

　なんでこんな事になってるんでしょう。HDR対応途中で止めてしまっているみたいにも見えるので今後腰を据えて対応するということかもしれません。それとも何か別に設定があるのか。8bitを指定しているのに実際の描画は16bitになってるのも妙ですが、とりあえずエディタもHDRに出来たので一安心です。
　この状態でPIE実行すると、PIEもHDR出力で動作することが確認できました。

:::note info
とあるコンソールプラットフォームではGRHIHDRDisplayOutputFormatにPF_ETC1という不正なフォーマット用の値がセットされるため、PLATFORM_WINDOWSで囲っています。
:::

## フィルミックトーンマップ

　公式ドキュメントによると

- フィルム マップ カーブは LDR コントロールとの相性が悪いため、HDR 出力中はデフォルトで無効されています。

　とのことです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/90d43e21-e3ef-90ca-bf37-9f80f233a7f1.png)
　確かにこの辺のパラメーターをいじっても画面には反映されません。SDR状態でフィルミックトーンマップに頼って調整してしまうとHDR時にけっこう困りそうです。

　なんの調整もせずにHDRに出力して問題ない絵づくりができれば良いのですが、なかなかそううまくは行かないと思います。HDR時のトーンマップをどうするかは今後の課題になりそうです。Eye AdaptaionやExposureはHDRもSDRも同じように機能するのですが、そのままHDRにしただけだと暗い画面になりがちです。今のUE4に足りないのはHDRに対応した調整可能なトーンマッパなのかな？

　ところで、コマンドラインから`r.TonemapperFilm`でフィルミックトーンマップの有効無効を切り替えることができるのですが、HDRでこの操作を行うとフィルミックトーンマップが適用されないはずなのに画面の色味が若干変化したりします。これはコマンドでフィルミックトーンマップを無効にすると、モバイルトーンマッパが有効になったとみなされるために、シェーダー内でモバイルトーンマッパでは機能しない処理が走らなくなるからと思われます。

```PostProcessCombineLUTs.usf
	// Expand bright saturated colors outside the sRGB gamut to fake wide gamut rendering.
    if (!bUseMobileTonemapper)
	{
```
　この部分です。コメントの通り、sRGB領域で飽和する色域を無理やり広げる処理のようです。なのでHDRで効かないはずのフィルミックトーンマップですが明示的に無効にするのはやめておいたほうが良さそうです。


# まとめ
　そろそろ開発中のゲームのHDR対応やらなきゃなという事で調べ始めたHDRですが、わからないことだらけで。「HDRの画像はこうだ」みたいなのをSDRで見てもピンとこないし、HDRで描画されているものが本当に正しいのかも良くわからないし。なかなか難しいなと感じてます。とりあえずわかる範囲で調べたものですが、みなさんのお役に立てば幸いです。
　結果としてはゲームビルドのHDR設定もわかったし、ちょっとした修正をすることでエディタもHDR対応できました。それなりの時間をかけて色々調べましたが、実際に必要な作業は１分で終わるようなものでした。人生とはそんなものです。
　もうひとつ実感したのは、ディスプレイによって見た目がけっこう違うことです。会社にはそこそこのHDRディスプレイを支給してもらっていますが、自宅の２年位前に買ったHDRディスプレイでは色も明るさも全然及ばない。良いディスプレイで作業するのは当然としても、品質がイマイチなディスプレイでもそれなりに見えるようにするためには明るさの調整機能を組み込みたいところです。よくある、画像のロゴと地が同じにになるように調整を３回繰り返すような。
　何はともあれ、我々は長年HDRでライティングしていたのに、そのままの状態を見るすべが無かったのですが、HDRディスプレイのおかげである程度HDRな状態を見ることができるようになったのは嬉しい限りです。

# おまけ
　ソースコードをあちこち見ていて発見したことを紹介します。

## 起動オプションでhdrを指定できる

　UE4の起動オプションに -hdr を設定するとr.AllowHDR=1と同等の状態で起動する。iniを書き換えずに手っ取り早く試したいときには便利。

## OutputDeviceの種類が増えている

```
TAutoConsoleVariable<int32> CVarDisplayOutputDevice(
	TEXT("r.HDR.Display.OutputDevice"),
	0,
	TEXT("Device format of the output display:\n")
	TEXT("0: sRGB (LDR)\n")
	TEXT("1: Rec709 (LDR)\n")
	TEXT("2: Explicit gamma mapping (LDR)\n")
	TEXT("3: ACES 1000 nit ST-2084 (Dolby PQ) (HDR)\n")
	TEXT("4: ACES 2000 nit ST-2084 (Dolby PQ) (HDR)\n")
	TEXT("5: ACES 1000 nit ScRGB (HDR)\n")
	TEXT("6: ACES 2000 nit ScRGB (HDR)\n")
	TEXT("7: Linear EXR (HDR)\n")
	TEXT("8: Linear final color, no tone curve (HDR)\n")
	TEXT("9: Linear final color with tone curve\n"),
	ECVF_Scalability | ECVF_RenderThreadSafe);
```	
　ドキュメントには無いOutputDevice選択、7,8,9がありますね。デバッグ目的や画像出力用かな？

## 最大輝度は決め打ち

　ディスプレイの最大輝度は1000nitか2000nitのどちらかを手動で選択するしか無いみたいです。ディスプレイから最大輝度とか取得できないのでしょうか？

## 排他フルスクリーン

　ゲームビルドでBlueprint関数、EnableHDRDisplayOutputでHDR有効にすると強制的にフルスクリーンモードに切り替わります。以下の部分で強制的に切り替わるようになっています。

```GameUserSettings.cpp
void UGameUserSettings::EnableHDRDisplayOutput(bool bEnable, int32 DisplayNits /*= 1000*/)
// 中略
#if PLATFORM_WINDOWS
			if (IsRHIDeviceNVIDIA() || IsRHIDeviceAMD())
			{
				// Force exclusive fullscreen
				SetPreferredFullscreenMode(0);
				SetFullscreenMode(GetPreferredFullscreenMode());
				ApplyResolutionSettings(false);
				RequestUIUpdate();
			}
#endif
```
　NVIDIAかAMDのGPUの場合の処理になっていますが、IntelのGPUでもHDRはサポートされているはずなんですが。
　排他フルスクリーン、マルチディスプレイだとウィンドウがとっちらかって嫌なんですよね・・・。しかもコマンドでHDRに切り替える場合はここを通らないのでフルスクリーンになりません。

　公式ドキュメントには

- D3D11 の場合は、HDR への出力が排他的全画面のみに制限されています。D3D12 がサポートされている Windows 10 では、おそらく HDR 出力の個々のビューポートを有効にできるように拡張されると思われます。Mac では既に実装はサポートされています。

　とあります。確かにDirectX 12ではWindow単位でHDR出力できているようですが、DirectX 11でコマンドからHDRにする場合は事前にフルスクリーンにしておく必要があるのかもしれません。

## UE4エディタのUIの色はプロジェクト設定で変わる

　プロジェクト設定->Default Settings->フレームバッファピクセル形式が`10bit RGB, 2bit Alpha`の場合はSDR時とほぼ同じ明るさでエディタUIが表示されるが、`Float RGBA`の場合は明るいというか白っぽい感じになる。モードによってHDRぽくはなるが、高輝度でクリップしたりと正しい表示にはならない場合もあるようだ。まだまだ調査不足だが、4.27.2の時点でのエディタのHDR対応はまだまだ不完全なので、最終的な確認はビルドでやるのが良さそう。

# 参考文献

[HDR理論と実践](https://www.slideshare.net/nikuque/hdr-theory-and-practicce-jp)
[Direct3D12ゲームグラフィックス実践ガイド Pocol著](https://www.amazon.co.jp/dp/B09J9Z5ZKH/ref=cm_sw_r_tw_dp_KJG5REBXSJGN5DBNE4C8)
[高ダイナミックレンジ表示および高度な色での DirectX の使用](https://docs.microsoft.com/ja-jp/windows/win32/direct3darticles/high-dynamic-range)
[Windows HDR (とキャプチャ問題について) の解説](https://zenn.dev/tan_y/articles/55f63a1d97495d)