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ピクセル未満の範囲でサンプリング位置を変えて,それらを合成するのが基本的なアイディアです。
ただし,過去のフレームすべてを保持しておくことは行いません。
指数的に減少するような重みを採用して,指数移動平均とすることで,過去のフレームの積分である履歴バッファのみを保持します。
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
Jitter
カメラの透視投影行列を上下左右に1ピクセル未満のオフセットを加えます。
DX系でのオフセットの掛け方の例です。透視投影行列の計算のnear平面の四角形をずらせば導出できます。
この微妙なオフセットがジッターとして各フレームで時間的な遅れを持ちながらもマルチサンプリング的な効果を与えます。
ずらすオフセットの量ですが,Halton列を利用しました。
https://en.wikipedia.org/wiki/Halton_sequence
float HaltonSequence(uint32_t i, uint32_t b)
{
float f = 1.0f;
float r = 0.0f;
while (i > 0)
{
f = f / b;
r = r + f * (i % b);
i = i / b;
}
return r;
}
m_TaaCameraJitter.x = (HaltonSequence(m_TaaJitterIndex+1, 2) - 0.5f) * jitterScale;
m_TaaCameraJitter.y = (HaltonSequence(m_TaaJitterIndex+1, 3) - 0.5f) * jitterScale;
VelocityMap
履歴バッファを単にサンプリングしているだけだと,以前実装した残像表現の効果と同様にカメラやオブジェクトが動くと残像として尾を引くアーティファクトが生じてしまいます。
これを補正するために速度バッファを利用して,1フレーム前に参照されていたピクセルと同じ位置の履歴バッファを参照します。
ただし,速度が大きくなってしまうと,やはり誤差が大きくなって残像が出てきてしまうので,
速度ベクトルの長さを利用して動きの速いピクセルほど指数移動平均の重みを小さくすることで残像を消していきます。
また,単に速度ベクトルを使わないで,近傍のテクセルの中から,depthを参照して最も手前にある速度ベクトルを採用すると,うまくいくようです。
Tonemap
入力されたピクセルがあまりに高輝度だと,高輝度成分の緩和に時間がかかって,
フリッカーとしてチラついたり,まったくアンチエイリアスがかからなくなってしまったりと不都合が生じます。
理論上は無限時間かけて沢山サンプリングすれば軽減されるのですが,
高輝度成分をサチらせるようなトーンマップをかけてSDR空間上でTAAを行うことでも軽減できます。
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を取るものとしました。
は露光に対応する係数ですが,平均輝度がトーンマップによってKeyValueであるに移るように選んでいます。
Colorのクランプ,輝度差のある履歴の不採用
TAAはアンチエイリアスなので,原理的には入力となるピクセルの周辺3x3範囲からテクスチャをサンプリングされるはずなので,
少なくとも3x3範囲の色には無い高輝度な色や暗すぎる色はサンプルされないはずです。
周辺の色のmin,maxを取ってクランプすることで,残像を除去します。
また,履歴バッファと入力カラーの輝度差があまりにも大きいと,フリッカーの原因となるので,これを除去するような係数も入れます。
以上の処理を適用したシェーダーが次のものです。
const float2 uvT = uv + float2(0.0f, TAA_PIXEL_SIZE_Y);
const float2 uvB = uv + float2(0.0f, -TAA_PIXEL_SIZE_Y);
const float2 uvR = uv + float2(TAA_PIXEL_SIZE_X, 0.0f);
const float2 uvL = uv + float2(-TAA_PIXEL_SIZE_X, 0.0f);
const float srcLinearDepthT = DecodeFloatRG(SampleTextureLevel0(g_DepthNormalTexture, g_LinearSampler, uvT).ba);
const float srcLinearDepthB = DecodeFloatRG(SampleTextureLevel0(g_DepthNormalTexture, g_LinearSampler, uvB).ba);
const float srcLinearDepthR = DecodeFloatRG(SampleTextureLevel0(g_DepthNormalTexture, g_LinearSampler, uvR).ba);
const float srcLinearDepthL = DecodeFloatRG(SampleTextureLevel0(g_DepthNormalTexture, g_LinearSampler, uvL).ba);
float3 velocityUV = float3(uv, srcLinearDepth);
velocityUV = lerp(velocityUV, float3(uvT, srcLinearDepthT), step(srcLinearDepthT, velocityUV.z));
velocityUV = lerp(velocityUV, float3(uvB, srcLinearDepthB), step(srcLinearDepthB, velocityUV.z));
velocityUV = lerp(velocityUV, float3(uvR, srcLinearDepthR), step(srcLinearDepthR, velocityUV.z));
velocityUV = lerp(velocityUV, float3(uvL, srcLinearDepthL), step(srcLinearDepthL, velocityUV.z));
const float2 velocity = SampleTextureLevel0(g_VelocityTexture, g_LinearSampler, velocityUV.xy).rg * TAA_VELOCITY_DECODE.xy + TAA_VELOCITY_DECODE.zw;
const float2 historyUV = saturate(uv - velocity);
float3 historyColor = TonemapTAA(SampleTextureLevel0(g_HistoryTexture, g_LinearSampler, historyUV).rgb);
const float3 inputColor = TonemapTAA(outColor.rgb);
const float3 cT = TonemapTAA(SampleTextureLevel0(g_SrcTexture, g_LinearSampler, uvT).rgb);
const float3 cB = TonemapTAA(SampleTextureLevel0(g_SrcTexture, g_LinearSampler, uvB).rgb);
const float3 cR = TonemapTAA(SampleTextureLevel0(g_SrcTexture, g_LinearSampler, uvR).rgb);
const float3 cL = TonemapTAA(SampleTextureLevel0(g_SrcTexture, g_LinearSampler, uvL).rgb);
const float3 cMin = min(inputColor,min(min(cT, cB), min(cR, cL)));
const float3 cMax = max(inputColor,max(min(cT, cB), max(cR, cL)));
historyColor = clamp(historyColor, cMin, cMax);
const float velocityHistoryColorFactor = saturate(length(velocity) * TAA_ALPHA_VELOCITY_SCALE + TAA_ALPHA_VELOCITY_OFFSET);
float alpha = lerp(TAA_ALPHA_STATIC, TAA_ALPHA_DYNAMIC, velocityHistoryColorFactor);
const float lumaHistoryColorFactor = saturate( abs(GetLuminance(historyColor) - GetLuminance(inputColor)) * TAA_ALPHA_LUMA_SCALE + TAA_ALPHA_LUMA_OFFSET);
alpha *= lumaHistoryColorFactor;
outColor.rgb = InvTonemapTAA(lerp(inputColor, historyColor, alpha));
結果
TAA OFF |
TAA ON |
TAA OFF |
TAA ON |
なんだかアンチエイリアスが掛かっている気がします。
画面を動かしても不自然なゴーストはできません。
しかし,動画としてみると,高周波な部分がジッターで若干画面がザラつくような効果があるので,もっと調整が必要かもしれません。