拙訳
基本のアイデア(Basic Idea)
MLAAと仲間たちはバックバッファ(と任意に深度バッファ)からエッジを復元する。
アイデア:
ゲームエンジンはエッジのある所を知っている。
それを使おう!
第1の試み --- GPAA(First Attempt - GPAA)
- 最終画像にエッジを上描きする。
- 主要な方向(水平/垂直)を確定する。
- カバレッジを計算する。
- 適切な近傍でブレンドする。
- テクスチャフィルタに最適化する。
- シーンのジオメトリを事前処理する。
- 関連するエッジを抽出する。
プリプロセスは表面にある内部エッジを除外し、共有されたエッジから重複分を取り除く。
線の式はスクリーン空間で計算され、ピクセルシェーダに渡される。
GPAA --- シェーダ(GPAA - Shader)
// 幾何的な線の間の差異を計算し、位置をサンプルする。
float diff = dot(In.KMF.xy, In.Position.xy) + In.KMF.z;
// 近傍の表面のカバレッジを計算する。
float coverage = 0.5f - abs(diff);
float2 offset = 0;
if (coverage > 0) {
// 方向を選択して、近傍ピクセルをサンプルする。
flot off = (diff >= 0) ? 1 : -1;
if (asint(In.KMF.w))
offset.y = off;
else
offset.x = off;
}
// テクスチャフィルタリングと座標ずらしを適切に用いて近傍ピクセルをブレンドする。
return BackBuffer.Sample(Filter, (In.Position.xy + coverage * offset.xy) * PixelSize);In.KMFは水平か垂直のいずれかの方向に対する線の式を含む。方向フラグがwに格納される。
サンプリング方向は線がピクセル中心をどちら側に切り取るかに基づいて選択される。
カバレッジは距離から計算される。
GPAA --- 結論(GPAA - Conclusions)
- 非常に高品質
- 非常に正確なカバレッジ
- 水平/垂直に近いケースに優れる。
- 時間的に安定
- エッジ抽出ステップ
- 不便
- メモリ消費を増加させる
- 線のラスタライゼーション
- パフォーマンス的に理想的でない
- 増加する幾何的密度のスケーリング問題
ソース付きデモがhttp://www.humus.nameでダウンロードできる。
第2の試み --- GBAA(Second Attempt - GBAA)
- メインパスでレンダターゲットにジオメトリ情報を格納する。
- ジオメトリの事前処理なし
- 線のラスタライゼーションなし
- 小さな一定メモリの追加
- フルスクリーン”解決”パス
- 固定のコスト
ジオメトリバッファはフルスクリーンで、2チャンネルの、符号付きフォーマットである。必要ならば1チャンネルにパックできるだろう(1ビットは水平/垂直のためのフラグを格納する)。
GBAA
- ジオメトリシェーダはジオメトリ情報を伝達する。
- 主要な方向でのエッジへの距離を格納する。
- 線の式の計算ではなく補間器を用いる。
noperspectiveキーワードを用いる。
- ピクセルシェーダは最も近いエッジを選択する。
d_dir = d / (|n.x| > |n.y| ? n.x : n.y)
エッジ上の2つの頂点は当然そのエッジへの距離が0である。
上の頂点は、そのエッジが横なので、縦の距離が割り当てられる。
垂線の長さdが初めに計算され、縦の距離dxにするファクタでスケールされる。
GBAA --- 解決(GBAA - Resolve)
- ピクセルごとに、交差するエッジに対してバッファを調べる。
- 距離がピクセル半分より小さいならば、1を得る。
- 近傍を選択し、カバレッジを計算し、ブレンドする。
- そうでなければ、ピクセルを据え置く。
- 距離がピクセル半分より小さいならば、1を得る。
- 問題: 輪郭エッジでのギャップ
これらのギャップは片方のエッジのみが関連するジオメトリ情報を持つために起こる。背景の表面は前景のジオメトリについて知らない。前景はエッジに向かって適切にブレンドするが、通過するエッジの兆候を持たないため、背景はまったくブレンドされない。これは結果として輪郭のエッジに沿ったアンチエイリアシングでの可視ギャップになる。隣接トライアングル間のエッジは、両側がエッジを認識するので、影響を受けない。
- 解決法
- 最も近いエッジに対する周囲の近傍を検索する。
- 以下ならば、現在のピクセルに一致するエッジを用いる。
- 左: offset.xが
- 右: offset.xが
- 上: offset.yが
- 下: offset.yが
我々の方を指す可能性がある要素を調べることのみが必要となる。左/右に対して、X要素がこのピクセルと合致するかを確認し、上/下に対して、Y要素がこのピクセルと合致するかを確認する。近傍の距離の値がこのピクセルを切り取るようであれば、このエッジ情報を用いる。
この図では、左のピクセルが関連するエッジ情報を保持する。この値は多分0.8である。これは0.5(ピクセルの左エッジ)より大きく、1.0(ピクセル中心)より小さい。これは0.8 - 1.0 = -0.2の距離をもたらす。すなわち、エッジはピクセル中心の左に0.2ピクセル単位のところを通り抜ける。
この図では下のピクセルは右を指すことにも注意する。故に、有用であることを強調しなければならないので、使い道がない。右と上は同じ表面に属し、いずれもこの状況では有用な情報を持たない。
- エッジが復元した!
GBAA --- シェーダ(GBAA - Shader)
float2 offset = GeometryBuffer.Sample(Point, In.TexCoord).xy;
// エッジがピクセルと交差するかを調べる。そうでないなら、近傍を検索する。
《flatten》 if (max(abs(offset.x), abs(offset.y)) > 0.5f) {
offset = 0.0f;
float2 offset0 = GeometryBuffer.Sample(Point, In.TexCoord, int2(-1, 0)).xy;
float2 offset1 = GeometryBuffer.Sample(Point, In.TexCoord, int2( 1, 0)).xy;
float2 offset2 = GeometryBuffer.Sample(Point, In.TexCoord, int2( 0, -1)).xy;
float2 offset3 = GeometryBuffer.Sample(Point, In.TexCoord, int2( 0, 1)).xy;
if (abs(offset0.x - 0.75f) < 0.25f) offset = offset0.xy + float2(-1, 0);
if (abs(offset1.x + 0.75f) < 0.25f) offset = offset1.xy + float2( 1, 0);
if (abs(offset2.y - 0.75f) < 0.25f) offset = offset2.xy + float2( 0, -1);
if (abs(offset3.y + 0.75f) < 0.25f) offset = offset3.xy + float2( 0, 1);
}
float2 off = (offset >= float2(0, 0)) ? float2(0.5f, 0.5f) : float2(-0.5f, -0.5f);
offset = offset ? off - offset : offset;
// テクスチャフィルタリングと座標ずらしを適切に用いてピクセルを近傍ピクセルとブレンドする。
return BackBuffer.Sample(Linear, In.TexCoord + offset.xy * PixelSize);GBAA --- アルファテスト(GBAA - Alpha test)
- GBAAは、距離が計算/推定できるならば、いかなるエッジでもアンチエイリアシングできる。
- アルファテストされたエッジは勾配で推定する。
- 内部シェーダのエッジ
- パララックスオクルージョンマッピング
float dx = ddx(alpha);
float dy = ddy(alpha);
bool major_alpha_dir = abs(dx) > abs(dy);
float alpha_dist = -alpha / (major_alpha_dir ? dx : dy);アルファテストは上下左右にテクスチャ座標の勾配とサンプルテクスチャを取ることでより良い推定ができる。これはよりコストが高いが、上記が実践において十分に良く動作する。
GBAA --- 結果(GBAA - Results)
GPAAと同じくらいの結果だが、アルファテストされた表面でもアンチエイリアシングしている。
GBAA --- パフォーマンス(GBAA - Performance)
解決パス:
| 1280x720 | 0.09ms |
| 1920x1080 | 0.18ms |
| 2560x1600 | 0.36ms |
: GPU: Radeon HD 5870
解決パスは安価で、かなり一定のコストを持つ。私はclip()を用いて、幾分かパフォーマンスを改善するよう、バックバッファへの書き込みを削減した。ただし、これは密なジオメトリでは最適化にならないかもしれない。
これらの数は解決パスのみであることに注意。このテクニックは主なシーンレンダリングの間のコストも負う。これはコンテンツ依存であり、シーンシーンで変化するかもしれない。
今後の課題(Future work)
- DX9/コンソール
- 内部エッジ
- ピクセルあたり複数エッジ
- 複数の近傍をブレンドする。
- OIT付きDX11
- 近傍ではなく背景でブレンドする。
現在の実装はジオメトリシェーダ、DX11に依存する。DX9で実装できるかもしれないが、メモリの観点でよりコストが高くなるだろう。これ周りの方法は興味深い研究項目になるだろう。
内部エッジはアンチエイリアスするべきではない。現在の実装は行っている。重大な損害を与えるようには思えないが、理想的には回避すべきである。
コーナーやTジャンクションは小規模のアーティファクトを時々引き起こす可能性がある。すべてのエッジを用いてより多くの近傍をブレンドすることはおそらくこれらのケースを解消する可能性がある。
深度でのブレンディングはサイドへのブレンディングより正確である。より実践的になったorder-independent-translucencyでは、これは将来にpost-AAで行われる方法かもしれない。
おわりに(Conclusion)
- 非常に高品質なアンチエイリアシング
- 低い固定のメモリコスト
- 安価な解決パス
- メインレンダリングで変化するコスト
- 未だ研究中
- 最新の結果は<www.humus.name>を確認してね