Temporal Anti-Aliasingの実装

Temporal Anti-Aliasing

Temporal Anti-Aliasing(TAA)とは,アンチエイリアスの一種です。
前に組み込んだ,FXAAとかSMAAは1フレームの情報から, スクリーンスペースで画像処理的に行うアンチエイリアスでした。TAAは過去のフレームも使ってサンプリング数を稼いで行うアンチエイリアスです。

http://advances.realtimerendering.com/s2014/index.html#_HIGH-QUALITY_TEMPORAL_SUPERSAMPLING
https://github.com/Unity-Technologies/PostProcessing/blob/v2/PostProcessing/Shaders/Builtins/TemporalAntialiasing.shader

過去のフレームほど,重みを小さくして,1ピクセル未満の範囲でサンプリング位置を変えて,それらを合成するのが基本的なアイディアです。

f:id:hikita12312:20180722095207p:plain:w600

ただし,過去のフレームすべてを保持しておくことは行いません。 指数的に減少するような重みを採用して,指数移動平均とすることで,過去のフレームの積分である履歴バッファのみを保持します。
https://ja.wikipedia.org/wiki/%E7%A7%BB%E5%8B%95%E5%B9%B3%E5%9D%87#%E6%8C%87%E6%95%B0%E7%A7%BB%E5%8B%95%E5%B9%B3%E5%9D%87

{ \displaystyle
\begin{eqnarray}
[\mathrm{Output}] &=& \alpha [\mathrm{Input}] + (1-\alpha) [\mathrm{History}] \\ 
[\mathrm{History}] &\leftarrow& [\mathrm{Output}]
\end{eqnarray}
}

Jitter

カメラの透視投影行列を上下左右に1ピクセル未満のオフセットを加えます。

{ \displaystyle
\begin{eqnarray}
a &=& \mathrm{aspect} \\
\theta &=& \mathrm{Fov Y} \\
f &=&  \mathrm{far Z}\\
n &=&  \mathrm{near Z}
\end{eqnarray}
}
{ \displaystyle
\begin{eqnarray}
\left(  
\begin{array}{cccc}
\frac{1}{a \tan\theta } & 0 & -2\mathrm{Offset_x} & 0 \\
0 & \frac{1}{\tan\theta } & -2\mathrm{Offset_y} & 0 \\
0 & 0 & \frac{f}{f-n} & -\frac{f n}{f-n} \\
0 & 0 & 1 & 0
\end{array}
\right)
\end{eqnarray}
}

DX系でのオフセットの掛け方の例です。透視投影行列の計算のnear平面の四角形をずらせば導出できます。
この微妙なオフセットがジッターとして各フレームで時間的な遅れを持ちながらもマルチサンプリング的な効果を与えます。

ずらすオフセットの量ですが,Halton列を利用しました。
https://en.wikipedia.org/wiki/Halton_sequence

VelocityMap

履歴バッファを単にサンプリングしているだけだと,以前実装した残像表現の効果と同様にカメラやオブジェクトが動くと残像として尾を引くアーティファクトが生じてしまいます。 これを補正するために速度バッファを利用して,1フレーム前に参照されていたピクセルと同じ位置の履歴バッファを参照します。

ただし,速度が大きくなってしまうと,やはり誤差が大きくなって残像が出てきてしまうので, 速度ベクトルの長さを利用して動きの速いピクセルほど指数移動平均の重みを小さくすることで残像を消していきます。

また,単に速度ベクトルを使わないで,近傍のテクセルの中から,depthを参照して最も手前にある速度ベクトルを採用すると,うまくいくようです。

Tonemap

入力されたピクセルがあまりに高輝度だと,高輝度成分の緩和に時間がかかって, フリッカーとしてチラついたり,まったくアンチエイリアスがかからなくなってしまったりと不都合が生じます。 理論上は無限時間かけて沢山サンプリングすれば軽減されるのですが, 高輝度成分をサチらせるようなトーンマップをかけてSDR空間上でTAAを行うことでも軽減できます。

{ \displaystyle
\begin{eqnarray}
k &=& \frac{a}{(1-a)L_{\mathrm{mean}}}
\end{eqnarray}
}
float3 TonemapTAA(float3 color)
{
    color *= k;
    return color / (1.0f + max(color.r,max(color.g, color.b)) );
}

float3 InvTonemapTAA(float3 color)
{
    color = color / (1.0f - max(color.r, max(color.g, color.b)) );
    return color / k;
}

このトーンマップ関数は,輝度に対するReinhard関数となっています。 逆トーンマッピング時に発散しないように,輝度は内積を使うものではなく,maxを取るものとしました。
kは露光に対応する係数ですが,平均輝度L_{\mathrm{mean}}がトーンマップによってKeyValueであるa=0.18に移るように選んでいます。

Colorのクランプ,輝度差のある履歴の不採用

TAAはアンチエイリアスなので,原理的には入力となるピクセルの周辺3x3範囲からテクスチャをサンプリングされるはずなので, 少なくとも3x3範囲の色には無い高輝度な色や暗すぎる色はサンプルされないはずです。 周辺の色のmin,maxを取ってクランプすることで,残像を除去します。

また,履歴バッファと入力カラーの輝度差があまりにも大きいと,フリッカーの原因となるので,これを除去するような係数も入れます。

以上の処理を適用したシェーダーが次のものです。

結果

f:id:hikita12312:20180722192934p:plain:w300
TAA OFF
f:id:hikita12312:20180722192956p:plain:w300
TAA ON
f:id:hikita12312:20180722193041p:plain:w300
TAA OFF
f:id:hikita12312:20180722193058p:plain:w300
TAA ON

なんだかアンチエイリアスが掛かっている気がします。 画面を動かしても不自然なゴーストはできません。 しかし,動画としてみると,高周波な部分がジッターで若干画面がザラつくような効果があるので,もっと調整が必要かもしれません。