DirectXの話 第136回

Temporal Coherence (Temporal Reprojection)

久しぶりの更新ですがまるで大きなことはやらず、誰もが知ってそうな既存技術を実装してみました。

まあ、簡単に実装出来そうなネタが無かったというだけの話だったり。

あと、引っ越しやら仕事やらで色々…はい、言い訳ですね。

今回の実装技術はTemporal Coherenceです。

日本語では時間干渉性となるらしいですが、CG用語ではなく物理関係の用語っぽいです。

Temporal Reprojectionとも呼ばれますが、こちらの方が呼び方としては一般的かもしれません。

私が最初に名前を聞いたのがTemporal Coherenceだったので、ここではそう呼ぶようにします。

この技術はどういう物かというと、歴史としては割と古くて『Gears of War 2』のSSAOにはすでに用いられています。

簡単に言えば、前回フレームの情報をそのまま使って、1フレームで処理するには重い処理を数フレームに分割しちゃいましょう、という物です。

前回フレームの情報を利用する、というと思い浮かぶのはフィードバックブラーという人もいるでしょう。

これは前回のフレームバッファを今回のフレームバッファに任意のアルファ値で重ね合わせる技法で、カメラブラーっぽさを手軽に得られる技術です。

今回紹介するTemporal Coherenceはこれとはちょっと違います。

シーンフレーム間においてある程度の連続性が保たれている場合、今回フレームで描画される任意の3D座標は前回フレームでも描画されている可能性が高くなります。

カットの切り替え、オブジェクトの消滅、オブジェクトによる遮蔽などによって描画されないこともあり得ますが、

カット切り替え以外ではシーンフレームの半分以上のピクセルが前回フレームでも描画されていると見て間違いないのではないでしょうか?

そこで、今回フレームの各ピクセルを3Dワールド座標に戻し、これを前回フレームのView、Projection行列で変換すれば前回フレームでのフレームバッファの結果を取得出来る、と言うことになります。

カメラが画面下方向に移動して、シーンが(a)から(b)に変化したと考えてください。

点Aは前回フレーム(a)でも今回フレーム(b)でもスクリーン上に投影されています。

点Bはどうかというと、今回フレーム(b)でも視野錐台に入ってはいますが、球によって遮蔽されてしまったため、スクリーンに投影されるのは点B'となり、前回フレーム(a)の情報は使えません。

点Cは今回フレームではスクリーンの外に出てしまいました。前回フレームの点Cの情報は使えなくなりました。

もしも(b)から(a)に変化したとしたら、点B、Cの前回フレーム情報は存在しないと言うことになるので通常の計算が行われます。

スクリーンに投影された座標が前回フレームでも描画対象になっていたかどうかを調べるには前回フレームの深度値を利用します。

今回フレームの3D座標を前回フレームのView、Projection行列で変換すると前回フレームでの深度値を求めることが出来ます。

この深度値と前回の深度バッファの差をチェックし、一定以上の差がなければ前回フレームの情報を利用し、そうでない場合は前回フレームの対象ピクセルを棄却します。

もしも深度値だけの判断では信用出来ない、という場合は法線方向や、マテリアルIDなんかもつかってチェックすれば良いでしょう。

前回フレームの情報を採択した場合、どのような計算を用いれば良いのでしょうか?

計算式はわかりやすくするため、レイマーチング法によるSSAOを例に取ります。

通常、レイマーチング法によるSSAOはAOの値を以下の計算式で求めます。

f()はレイを飛ばした際のAOの計算処理で、遮蔽されていれば0を、遮蔽されていなければ1を返す関数とお考えください。

Rnはレイです。つまり、f(Rn)はn番目のレイが遮蔽されているかいないかを調べている処理と言うことです。

最終的にはN本のレイを飛ばし、平均を求めたものをAOとしています。

レイマーチング手法ではこのレイの本数を多くすればAOの品質は向上しますが、1フレームに付き16本も飛ばせばかなりの処理時間が食われます。

数百本から千本くらい飛ばせばブラーパスを使用しなくとも品質の高いAOを求められるのですが、さすがに1フレームでは無理というもの。

しかし、以下の計算式を用いることで、(理論上は)数千本のレイを飛ばすことが出来るようになります。

frame, frame-1と付けられたAOはそれぞれ、今回のフレームで計算されたAO、前回フレームまでで計算されたAOです。

つまり、この計算結果のAOは次回フレームではAO_frame-1にフィードバックされます。

Nは前の式と同様に飛ばすレイの最大本数です。n_totalは前回フレームまでで飛ばされたレイの本数、n_currentは今回フレームで飛ばされたレイの本数です。

つまり、n_total + n_currentが次回フレームのn_totalになるわけです。

1フレームで飛ばすレイの本数は1本以上であれば何本でもかまいません。処理速度と相談してください。

1フレームで飛ばすレイの本数が多ければ最終結果に収束する速度が速まりますが、処理時間は増えます。

この技術の弱点としては、保存しておかなければならないバッファが多く、その分メモリ的にコストがかかるという点です。

前回フレームまでのレイの本数を保存しておかなければならないので、AOバッファは1ピクセルあたり2チャンネルが必要になります。

また、前回の深度バッファも保存しておく必要があります。なお、サンプルでは深度・法線バッファを保存しているのでその分重いです

では、サンプルを以下のURLからDLしてください。

Download:Sample127.zip

操作はカメラ操作がマウスとキーボードのFPS操作になっていて、マウス左ドラッグでカメラ回転、WASDで前後左右の移動、マウス右ドラッグでライトの回転となります。

デフォルトではTemporal CoherenceはOFFになっています。

ONにすると、HDAOとMulti-View Soft ShadowがTemporal Coherenceで動作します。

自宅のRadeonHD 5870環境では、OFF時に60fps前後、ON時に500fps以上で動作します。

カメラやライトが動いていない状態では映像品質がほとんど変わりません。

実装上の注意がいくつかあります。

まず、前述の例で言うところのNの値はとことんまで大きくして良いのかという点です。

これは一概にはそうとは言い切れない部分があります。

Nが少ない場合、当然品質の問題も出てくるのですが、それ以上に結果の安定性に問題が発生しやすくなります。

サンプルでHDAO Sampleの項目を1.0にするとわかりやすいですが、AOの結果がだいぶ揺れ動きます。デフォルトの2.0でもよく見れば揺れているのがわかるはずです。

このようになる理由としては、レイ1本のAOに対する寄与度がNが少ない場合は高くなってしまうと言うことが原因です。

前回フレームのAOが0.5だったとします。すでに最大数のレイが飛ばされていると仮定し、Nが16の場合と512の場合で、1フレームに1本のレイが計算されるとしましょう。

今回フレームのレイが遮蔽されているとすると、それぞれの結果は以下のようになります。

AO = (0.5 * 16 + 0 * 1) / (16 + 1) = 0.471 (N = 16)

AO = (0.5 * 512 + 0 * 1) / (512 + 1) = 0.499 (N = 512)

0.02の差が大きいと取るか小さいと取るかは難しいところですが、N=512の方が安定していることがわかります。

今回のサンプルのHDAOはリングの使用するポイントをかなり規則正しくTemporal Coherenceで処理しています。

安定性が低い理由の一つは間違いなくこれなんですが、安定性がわかりやすくなるためにあえてこのままにしています。

多分、それなりのランダム性を持たせれば、もう少しは安定するはずです。と言っても、それほど大きな差は出ないと思いますが。

ではNを単純に大きくするとどうなるでしょう?この場合は残像効果が大きく出てきてしまいます。

サンプルではシャドウが非常にわかりやすいので、Shadow Sampleを最大にしてライトを動かしてみてください。

Temporal CoherenceがON/OFF時で残像の出方が違うように見えるのではないでしょうか。

もちろんこれは錯覚ではありません。

完全に影になっている任意の座標がライトの移動によって影ではなくなったとします。

サンプルでは1フレームに付き1枚のシャドウマップを適用するようにしていますので、数フレーム経過した程度では影が薄くなるという状態にしかならないのです。

実装時には結果がギリギリ安定するNを用いるのが良いのではないでしょうか?

もう1つの注意点として、Reprojection時のテクスチャフェッチはLinearサンプリングを使用しましょうというものです。

Reprojectionは理論上は正しい結果が出てくることにはなるのですが、実際にはフレームバッファの解像度の影響を受けます。

これはシャドウマップの解像度が粗いとアーティファクトが出てきてしまうのと原因は一緒です。

当初Pointサンプリングでテクスチャフェッチしていたのですが、これだと特にシャドウマップの品質が悪くなっていました。

Linearサンプリングにすることで、残像っぽい減少を低減することが出来ます。

シーンの変化が大きく、且つ高頻度な場合は安定性や残像問題などが顕著に出てくる場合があります。

また、カメラの移動速度が速すぎるとほとんどReprojectionが棄却されてしまうのであまりオススメ出来ません。

レースゲームなんかだと1フレームに進む距離が非常に長かったりするので効果が薄くなる可能性があります。

極めて有効な技術ではあるのですが、どんな場面でも有効とは言えませんので自身が作成するゲームに適用しやすいようならするという感じで実装しましょう。

最後に、この技術は今世代の技術であり、次世代機(PS4やDurango)では不要ではないかと考える人もいるかと思います。

その意見もありだとは思うのですが、多分まだまだ現役で使われる技術になるんじゃないかと思っています。

まあ、この辺は個人的な意見ですけどね。