DirectXの話 第178回

FidelityFX Super Resolution vs NVIDIA Image Scaling

21/11/23 up

世はまさに超解像時代!

探してみろ!ネイティブ4K 60fpsが出せるハードウェアをそこに置いてきた!

GIとか当然やりたいよね。時間変化もしたいし、動的GIよろしく。あ、イテレーションに問題出るので事前にデータ生成するとかなしでね。
反射なんかも綺麗にしたいよね。今はレイトレってやつがあるんでしょ?それ使おう。
植生もいっぱい表示して、プレイヤーも100人同時、もちろん描画範囲も次世代機らしく広くしてね。
もちろん、4K 60fpsは当然だよ。某社さんも言ってたじゃない。それだけ出るハードだって。なんだったら120fpsも目指そうよ。

などという心無い言葉に対するグラフィクスエンジニアの怨嗟の声が聞こえてきますね。

現実として、現行ハードで4K 60fpsというのは大変厳しいです。
レイトレなんかやったら更に厳しいですし、なんならVRAMですら全然足りないのでは?と思われるくらい。
もちろんGeforce RTX 3090くらい使えば調整によっては出せると思いますが、そんなハイエンドをターゲットにするわけにもいかないわけです。

結局無難なところに落ち着けようとすると、最適化した上で1440pか、ダメなら1080pでレンダリングしましょう、となるわけです。

それでも4K出力や、今後の8K出力なんかも考えると、内部解像度が低くても出力は高解像度にするという選択肢が生まれます。
その結果として需要が高まっているのが超解像技術なわけです。
低解像度でレンダリングした結果を高解像度にアップスケールして4Kなり8Kなりの出力を実現しようという魂胆です。

現在最も注目されているのはやはりDeep Learningを利用した超解像技術でしょう。
NVIDIA社が提供するDLSSはDeep Learningを利用した超解像技術です。

https://www.nvidia.com/ja-jp/geforce/technologies/dlss/

入力としては低解像度画像と速度バッファを受け取り、それらの情報とDeep Learningによりリアルタイムに高解像度を実現します。
残念なのはGeforce RTXシリーズ以降のGPUでなければ使えないという点です。
Deep Learningを専門的に取り扱うTensorコアがなければリアルタイムは実現が難しいようです。

また、今後で言うならIntelも同様のDeep Learningを利用したXeSSという超解像技術を開発中らしいです。
こちらもハードウェアがIntelの次世代GPUに限定される可能性が高いため、誰でも恩恵を受けられるというわけでもなさそうです。

これらを考えると現在のコンソールゲーム機はこれらの技術を利用できないということになります。
UE5に実装される Temporal Super Resolution (TSR) はDeep Learningを利用していない技術ではありますが、UE5を利用しているプロジェクトならともかく、自社エンジンなどでは利用できません。
似たような技術を実装するという方法もなくはないですが、結構難しいとは思います。

自社エンジン勢の光明

そんな中、自社エンジン勢にも一筋の光明が燦然と輝いたわけです。
AMDが開発した FidelityFX Super Resolution (FSR) です。

https://github.com/GPUOpen-Effects/FidelityFX-FSR

FidelityFXはAMDが公開しているリアルタイムレンダリング向けの技術実装ブランドで、過去にもSSRやデノイザーなども公開しています。

FSRの最大の売りはシェーダが使えればどんなハードにも実装が可能ということです。
DLSSのように特定ハードでなければ利用できない、ということはほぼありません。
さすがにDX12にも対応していない古いハードになると難しいかもしれませんが、現行のハードであればメーカーを問わず使用できます。
もちろん、コンソールゲーム機も例外ではありません。
しかもDLSSと違って入力には低解像度画像以外は求めません。速度バッファは不要です。
パフォーマンスも非常に優れています。

ただし弱点としてはDLSSと比べると映像のアップスケールクオリティは低く、あまりの低解像画像からは綺麗にアップスケールできません。
また、DLSSはTAAの役割も担っています。つまり、DLSSを使う場合は特にアンチエイリアシングを行う必要はありません。
しかしFSRはアンチエイリアシングを行わないため、アップスケール前に別途アンチエイリアシングを必要とします。

下の画像はFSRのドキュメントから参照したFSRの実装イメージです。

アンチエイリアシングやトーンマッピングのあとにFSRを実行します。
ポストプロセスは種別によってFSRの前後に振り分けます。
画像にあるようにフィルムグレインなどは低解像度でレンダリングしてはよろしくないので、FSRのあとになります。
ブルームや被写界深度はFSRの前に行うほうが良いでしょう。

さて、FSRは映像クオリティ的には少し問題はあるものの、パフォーマンスと実装のしやすさという点では他の技術よりも上です。
コンソールゲーム機も扱わなければならない多くのゲームアプリケーションにとってはDLSSは使えないのでFSRが福音とも言えるでしょう。
しかし、それに待ったをかけた会社が出現しました。
DLSSを開発したNVIDIAです。

NVIDIAはつい最近、NVIDIA Image Scaling (NIS) を発表しました。

https://github.com/NVIDIAGameWorks/NVIDIAImageScaling

DLSSと違ってシェーダさえ使えればハードを選ばないアップスケーラーです。
FSRと同様にアップスケールだけを行うものなので、アンチエイリアシングは別途実装をする必要があります。
描画パスへの追加もFSRと同様と考えて良いでしょう。
アプリ側から提供する入力はやはり低解像度画像のみで、速度バッファが不要なのも実装しやすいでしょう。
まさにFSRキラーと言えます。

この2つのアップスケーラーはどちらもMITライセンスであるという点も重要です。
自社エンジンに組み込みやすいライセンスですね。

実装してみよう!

というわけで今回のサンプルではこの2種類のアップスケーラーを実装しました。
ベースはSample026を利用しています。
今回はTAAも実装していますが、これはFSRでも使われている Cauldron というAMDのサンプル作成用ライブラリから拝借しています。
FSRもNISも、TAAとSDRへの変換までは同一処理となっています。

まずはFSRの実装です。
FSRはEASURCASという2つのパスを実行する必要があります。
Edge Adaptive Spatial Upsampling (EASU) がアップスケーリングを行います。
つまりこのパスは入力が低解像度画像ですが、出力は高解像度画像です。
Robust Contrast Adaptive Sharpening (RCAS) は高解像度画像に対してシャープニングを行います。
こちらは入力が高解像度で、出力も同じ解像度です。
Contrast Adaptive Sharpening (CAS) というものもFidelityFXには存在しますが、RCASはこのCASのスケーリングを考慮しないバージョンのようです。

FSRとして提供されているファイルは2つのヘッダファイルです。
ffx_a.h はFidelityFXで使用されている変数などの定義です。
ffx_fsr1.h はFSRの実装です。このファイルはシェーダとC++側で参照することになります。

シェーダでは2つのヘッダをインクルードしますが、インクルードする前に #define ディレクティブを利用してどのような設定でFSRを使用するかを指定する必要があります。
また、一部の関数は未実装となっているため、これもヘッダをインクルードする前に実装しなければなりません。
該当のシェーダ部分は以下のように実装します。

A_GPU はシェーダで利用する場合には必ず必要になります。
A_HLSL はHLSL言語を使用する場合です。FSRはGLSLにも対応しているため、明示的に設定する必要があります。
FSR_EASU_F は浮動小数点をF32として EASU に利用するという設定です。半精度(F16) で良い場合は FSR_EASU_H を利用しましょう。
RCASにシェーダを利用する場合は FSR_RCAS_F, FSR_RCAS_H を利用します。
FsrEasuRF(), FsrEasuGF(), FsrEasuBF() の2つの関数はそれぞれの色成分で4ピクセル分の値を取得します。
定数バッファ名、シェーダリソース名については規定はありません。ユーザーがそれらを使用する関数などを提供するためです。

シェーダ内で利用する関数は FsrEasuF(), FsrEasuH(), FsrRcasF(), FsrRcasH() の4つです。
EASUパスかRCASパスか、また、F32かF16かで呼び出す関数を決定します。
引数はピクセル座標と定数バッファのみ。戻り値を出力すればそれでOKです。

CPU側でも2つのヘッダをインクルードします。
インクルードする前には A_CPU を定義しておく必要があります。シェーダ用の命令を参照しないようにするためです。
CPU側では定数バッファとして与える定数を求めます。
EASU用定数バッファはFsrEasuCon()関数を、RCAS用定数バッファはFsrRcasCon()を利用します。

シェーダ実行時のリソースは前述の関数で求めた定数と入力画像、サンプラー、出力画像のみです。
サンプルではFSRの実装はCompute Shaderで実装していますが、Pixel Shaderで実装しても構いません。
ね、簡単でしょ?

次にNISの実装です。
こちらはFSRと違って1パスで処理しますが、事前にいくつかの準備を必要とします。

シェーダ側ではやはりヘッダをインクルードする必要があり、インクルード前に定義などが必要になるのも同様です。

NIS_SCALER はNISでスケーリングを行う場合の設定です。
NISはアップスケーリングを行わずにシャープニングのみを行うこともでき、この場合は NIS_SCALER を0にします。
NIS_DXC はVulkanでNISを使用するための設定です。
NISは直接的にはVulkanに対応していませんが、DirectX Shader CompilerによるSpir-Vコンパイル時の特殊記述に対応しています。
NIS_DXC を 1 にすることでこの設定を付与することができます。
今回は不要なので 0 を指定しています。

NISはFSRほどシェーダコードに自由度がありません。
定数バッファはレジスタ番号や定数バッファ名自体には特に規定はありませんが、変数名は決められています。
定数名については NIS_Config.h に記述されているので、それを利用することをオススメします。
また、入力画像などのシェーダリソース名も固定されているので、その命名に従ってください。
命名については NIS_Scaler.h には特に記述がありませんので、SDKのサンプルを参照しましょう。

シェーダステージはCompute Shaderのみ利用可能です。Pixel Shaderには対応していません。
スレッド数などもサンプルに従うほうが良いでしょう。

CPU側では NIS_Config.h をインクルードします。
C++専用のヘッダファイルなので特に事前設定は不要です。

定数バッファは NVScalerUpdateConfig() 関数を利用します。
FSRのEASU定数更新と同様に描画解像度と出力解像度などを指定します。

入力するシェーダリソースは定数バッファ、入力画像、出力画像の他に係数を格納した2Dテクスチャを必要とします。
これは最初に作成すればOKで、毎フレーム更新する必要はありません。
NIS_Config.h の中に coef_scale, coef_usm という2次元配列が存在しており、これをテクスチャとしてシェーダに渡す必要があります。
この係数は浮動小数点で、8x64 の2次元配列です。
ただしテクスチャに格納する場合はRGBAにこの浮動小数点を格納するため、2Dテクスチャのサイズは 2x64 になります。
テクスチャフォーマットはRGBA_F32、もしくはRGBA_F16です。今回のサンプルではF32です。
注意点としては、このサイズのテクスチャを作成すると、テクスチャのRowPitchが256バイトとなります(Geforce RTXの場合)。
そのままテクスチャをマップしてコピーすると正常な値が取れなくなってしまうので、RowPitchに合わせて整形してコピーする必要があります。
今回はRowPitchを256の直値で実装してしまっているため、RadeonやIntel GPUでは正常動作しないかもしれません。

そしてこれはFSRとNISで共通することですが、これらを使ってアップスケーリングを行う場合はミップマップのLODにバイアスを掛ける必要があります。
ミップレベルはUV値のddx, ddyから求めるため、解像度が下がると自然とミップレベルが上っていく(低解像度テクスチャになっていく)ので、そのままだとテクスチャがボケた状態で描画され、アップスケーリングしても精細感が失われてしまいます。
そのため、高解像度ターゲットにレンダリングするときと同様の解像度でテクスチャをサンプリングする必要があります。

LODバイアスは以下の計算式で求められます。

LOD Bias = -log2(DisplayResolution / RenderResolution)

必ずこの値にしなければならないというわけでもありませんが、基本はこの値で、それでも精細感が足りないと思ったら追加のバイアスを掛けても良いでしょう。
ただし、あまりにバイアスを掛けすぎると精細感は上がるもののジラジラしたアーティファクトが発生するので注意が必要です。

サンプルを見てみよう

サンプルはいつものところにコミット済みです。

https://github.com/Monsho/D3D12Samples

Sample027が今回のサンプルです。
今回は動作を確認しやすいように実行ファイルも含めています。
Sample027/bin フォルダ内にある run.bat で実行できます。
ただし、内部でDXRを使用しているため、DXR非対応のGPUでは実行できない点に注意してください。

出力解像度は1440pで固定しています。
描画解像度は1440p、1080p、720pを選択できます。
アップスケーリングはただのBilinearとFSR、NISの3つから選択できます。
描画解像度が1440pの場合はFSR, NISを選択しても実行されません。

Sharpnessはシャープニングの強さを指定します。
NISは 0~1 の値で指定できるのでそのまま値を使用していますが、FSRは 0~2 の範囲で、しかも0が最もシャープな設定なので、0~1 の値を 2~0 にマッピングしています。

LODバイアスはOn/Offのみ指定可能です。
ただ、TAAのおかげでOn/Offしても差がわかりにくいと思います。
TAAを使用しない設定もあるので、TAAを切った状態で確認するとわかりやすいでしょう。

デフォルト設定で720pをアップスケールした場合の結果は以下のようになりました。

左からBilinear、FSR、NISです。
精細感、という点ではNISが抜きん出ていると感じさせます。
FSRはシャープニングを最大にすればなんとか、という感じです。
しかし1080pでの描画ならFSRも悪くありません。
描画解像度を表示解像度の70%くらいまでにするのであればFSRは十分選択肢に入るでしょう。

パフォーマンスはどうでしょうか?
Geforce RTX 2080では以下のようになりました。

Bilinearは当然高速ですが、FSRも十分高速です。
NISは描画解像度が1080pの場合に後塵を拝しています。
といっても720pになるとFSRと拮抗します。
FSRは描画解像度の依存は少ないようで、表示解像度に依存するようです。
逆にNISは描画解像度にも依存していることがわかります。内部で共有メモリを利用しているからかもしれませんね。

FSRを選ぶかNISを選ぶかは実装のしやすさ、パフォーマンス、最終的なクオリティを総合的に考えると良いでしょう。

どんな状況でも速度が欲しい場合はFSRの方が有利なようです。
しかしクオリティを取るならNISはかなりクオリティが高いと感じます。

実装のしやすさはFSRに軍配が上がります。
NISは自由度が少ないため、シェーダ記述に何らかのルールを設けているエンジンの場合は適合しない可能性もあります。
Compute Shaderが使えない環境ではFSRしか選べませんが、そもそもそのような環境でアップスケーラーが必要になるような高解像度を必要とするかは疑問でしょう。

また、コンソールゲーム機への実装という点ではNISは少し問題があるかもしれません。
HLSLをベースにしているエンジンであれば問題なさそうですが、GLSLベースのエンジンでは問題が出そうです。

NISはまだ公開されたばかりですので、実装のしやすさは今後改善される可能性もあります。
コンソールゲーム機への移植を行うチームもそのうち出てくるのではないでしょうか?
そのような人たちがプルリク投げればより良くなっていく可能性もあります。

というわけで、今回はここまで。
いい感じのアップスケーラーを自社エンジンに実装したい!という方はこれらを利用してみてはいかがでしょうか?