DirectXの話 第118回

Directionally Localized Anti-Aliasing

最近はAAばっかりですが、一応今回を最後にする予定です。

最後に紹介させていただくのは、LucasArts制作の『Star Wars : The Force Unleashed 2』で使用されている Directionally Localized Anti-Aliasing (DLAA) です。

作者はやはりLucasArtsの方で、GDC11にて発表されています。資料のサイトはこちら

基本的な考え方は、エッジが上下、もしくは左右に伸びているものと仮定し、その方向にブラーをかければいいんじゃないかというもののようです。

つまり、エッジが上下に伸びているなら垂直に、左右に伸びているなら水平にブラーをかけていくわけです。

もちろん、素直にそんなことをしていたら重くて仕方ありませんので、色々と工夫がされています。

この技術では、エッジに対して短めのエッジに対応する処理と長めのエッジに対応する処理の2種類を適用しています。

まずは短めのエッジから見ていきましょう。

ショートエッジの処理は5*5の十字型に行います。

自身のピクセルの上下左右それぞれ2ピクセルを取得し、水平方向、垂直方向それぞれに色の差からブレンド値を決定します。

最終的には水平方向、垂直方向の色の平均値と自身のピクセルをブレンド値で線形補間します。

極めて単純な処理ですが、シェーダコードは以下のようになります。

float4 center = texEdgeColor.SampleLevel( samPoint, inPixel.texCoord, 0 );

float4 left_s = texEdgeColor.SampleLevel( samLinear, inPixel.texCoord + float2(-1.5, 0.0) * uvOffset, 0 );

float4 right_s = texEdgeColor.SampleLevel( samLinear, inPixel.texCoord + float2( 1.5, 0.0) * uvOffset, 0 );

float4 top_s = texEdgeColor.SampleLevel( samLinear, inPixel.texCoord + float2(0.0, -1.5) * uvOffset, 0 );

float4 bottom_s = texEdgeColor.SampleLevel( samLinear, inPixel.texCoord + float2(0.0, 1.5) * uvOffset, 0 );

float4 w_h = 2.0 * (left_s + right_s);

float4 w_v = 2.0 * (top_s + bottom_s);

float4 edge_h = abs(w_h - 4.0 * center) / 4.0;

float4 edge_v = abs(w_v - 4.0 * center) / 4.0;

float4 blurred_h = (w_h + 2.0 * center) / 6.0;

float4 blurred_v = (w_v + 2.0 * center) / 6.0;

float edge_h_int = CalcIntensity( edge_h.rgb );

float edge_v_int = CalcIntensity( edge_v.rgb );

float blurred_h_int = CalcIntensity( blurred_h.rgb );

float blurred_v_int = CalcIntensity( blurred_v.rgb );

float edge_mask_h = saturate( (lambda * edge_h_int - kEpsilon) / blurred_v_int );

float edge_mask_v = saturate( (lambda * edge_v_int - kEpsilon) / blurred_h_int );

color = lerp( color, blurred_h.rgb, edge_mask_v );

color = lerp( color, blurred_v.rgb, edge_mask_h );

上下左右のピクセルは1回のフェッチで2ピクセル分を取得するため、リニアフィルタで取得しています。

w_h, w_v の計算で2倍している理由は、取得したカラーが2ピクセル分のカラーだからです。

CalcIntensity() はカラーを 1/3 で内積を取っているだけです。つまり、各成分の平均を求めています。

これらのパラメータからブレンド値 edge_mask_h, edge_mask_v を求めます。

このような計算にそれなりの理屈があるのか、それとも色々試した結果としてこの計算が良かったのかは不明です。

資料には、他にも色々な方法で対応可能、みたいに書いてあるので、トライ&エラーの結果かも知れませんね。

次に長めのエッジです。

ロングエッジの処理は2パスで行われます。

1パス目はハイパスフィルタで、画像の高周波成分、つまりエッジ部分を抽出するフィルタです。

このシェーダコードはこちらになります。

float4 RenderHighPassFilterPS( OutputVS inPixel ) : SV_TARGET

{

// 中心とその上下左右のカラーを取得する

float4 center = texColor.SampleLevel( samPoint, inPixel.texCoord, 0 );

float4 left = texColor.SampleLevel( samPoint, inPixel.texCoord, 0, int2(-1, 0) );

float4 top = texColor.SampleLevel( samPoint, inPixel.texCoord, 0, int2( 0, -1) );

float4 right = texColor.SampleLevel( samPoint, inPixel.texCoord, 0, int2( 1, 0) );

float4 bottom = texColor.SampleLevel( samPoint, inPixel.texCoord, 0, int2( 0, 1) );

// 色の差を整形してα値として描き込む

float4 edge = 4.0 * abs( (left + top + right + bottom) - 4.0 * center );

float a = CalcIntensity( edge.rgb );

return float4( center.rgb, a );

}

カラーの差が大きければ大きいほどアルファ値に大きな値が書き込まれます。

RGBにはそのままカラーを出力しますが、こうしておくとハイパスフィルタの結果とカラーを1回のテクスチャフェッチで取得できます。

次に実際にブラーを施すパスですが、ここで少し面白い工夫を行っています。

長いエッジを処理するため、自身の上下左右それぞれ8ピクセルを取得します。当然、リニアフィルタを利用しそれぞれ4回のフェッチで済ませます。

これらのピクセルのアルファ値が大きければ大きいほど、エッジが長くなっていると考えることができます。

このアルファ値をブレンド値として使用するのは正しいのですが、では、ブレンドするカラーはどうすればいいのか?

ここで、8ピクセルの平均を使用するのはもちろんよろしくありません。本来なら無関係な部分の色が滲んでしまう恐れがあるからです。

そのため、ブレンドカラーとして使用するものは自身のカラーと隣接ピクセルのカラーをブレンドしたカラーを用います。

どのようにブレンドするのか? そこにちょっとした工夫があります。

まず、8ピクセルの平均カラーをグレースケールに変更します。輝度を求めてもいいのですが、今回はそれぞれの成分の平均を求めています。

この値は…なんと自身のピクセルと隣接ピクセルの各グレースケールをブレンドした値だったんだよ、何だってー!、ということにします。

なんて無理やりな、と思われるでしょう。私も思いました。

もちろん、現実はそうではないのですが、そういうことにしておきます。

すると、自身と隣接をブレンドしたときのブレンド値が求まるわけで、このブレンド値を先のカラーブレンド値としても使用するというわけです。

計算式で見るとこんな感じ。

AverageLuminance = CenterLuminance * t + NeighborLuminance * (1 - t)

t = (AverageLuminance - NeighborLuminance) / (CenterLuminance - NeighborLuminance)

BlendColor = CenterColor * t + NeighborColor * (1 - t)

これでブレンドすべきカラーがわかったので、再び自身のカラーと上記で求めたカラーをハイパスフィルタの結果を用いてブレンドします。

こうして、ショートエッジとロングエッジの対応を行って求めたカラーが最終的なカラーとして出力されます。

ここでちょっと疑問に思われた方もいるかもしれません。

それは、エッジを求めているのがカラーだけど、深度や法線ではできないのか?、という点ではないでしょうか。

私もいくつかの手法を試してみたのですが、あまりうまくいきませんでした。

品質は悪くないけど速度に問題があったり、そもそもまともにアンチエイリアスがかかってなかったりと様々。

特に深度や法線でエッジを求めても(つまり、ハイパスフィルタで深度、法線を利用する)、その後の処理がカラーになっては意味がありません。

また、2パス目で深度、法線を使おうとすると、どうしてもテクスチャフェッチの回数が増えてしまい、速度が急激に低下します。

DLAAはカラーのエッジをアルファに格納し、カラーとハイパスフィルタの結果双方を同時に取得できるところが魅力です。

深度、法線を使用する場合、この利点がなくなってしまい、速度面で不利になってしまいます。

それならMLAA実装したほうがいいんじゃない?ってことになりますね。

速度面ではMLAAより高速ですが、うちの環境(RadeonHD5870)では驚くほどの違いは見せていません。

しかし、GDCの発表によると、Xbox360にて2.2ms程度で動くようです。

MLAAは4.0ms前後かかるそうなので、それに比べればだいぶ速いです。

なお、今回のサンプルではショートエッジとロングエッジの効果がわかりやすくなるようにするため、それぞれのON/OFFができるようにしています。

この分岐をなくせばもう少し高速化できます。

また、元のサンプルにはなかったのですが、ハイパスフィルタの結果が極めて小さい部分は2パス目の処理を省くようにしました。

これでも結果にそれほどの差異はなく、速度面でも向上しているのでよい結果といえるでしょう。

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

次回は何をやるか未定ですが、GPU Pro2から何か持ってくるかも。

それ以外にもAndroidいじりたいので。

あと、GoW3ベータテストが始まるので。