ピクセルシェーダ2.0で実装するZ値差ベースの輪郭線

今さらながら、どうして数年前にシェーダモデル2.0をサポートするのを必須にしたゲームがでまくったのかを理解した。単純に閾値による二分岐へ帰結せずにピクセルシェーダ上で様々に数値をいじろうとすると、この能力は1.xと2.0との間で大きな開きがある。今回のをps_1_1対応にしようとしてあまりに面倒くさく、結局instructionが増えるだけだったりして思い知った。

で、今はXP・Vista未満を切り捨てるDX10必須のPCゲームがでまくってるね。前回と同様に取り残されて動作環境を見るたび勝手に屈辱感を味わっている昨今。

概要

シャドウバッファと同様のアイデア。ぼかしで生じた滑らかな階調を保持したままで、アルゴリズムを介して画面へ適用してしまおうという企み。アンチエイリアスがかかった線が引けるのだから、モデルをふくらませて裏面を描く手法よりもきっと低解像度でも鑑賞に堪える綺麗なものができるだろう。近傍をいくつもサンプルして輪郭検出をしないからきっと速いだろう。

http://tpot.jpn.ph/t-pot/program/130_UnsharpMasking/index.html
大枠でやっていることはこれと同じ。

手順

  • 画面とアスペクト比が同じバッファへ輪郭線を出したい対象物のカメラからの(同サイズ望ましいが)Zを描く。例によって Out.Color = In.Pos.z * 4 - float4(3,2,1,0); によって各色成分へ値を割り振ってZ解像度を確保。
  • 例によってサンプラのリニアフィルタを効かせて数回拡大縮小してちょっとぼかす。あまりぼかすと輪郭線が太くなるのでほどほどに。ここでぼかす前の元バッファは潰さずに残しておくこと。
  • 対象物が普通に描画された後のポストエフェクトとして…
  • ぼかし前の元バッファとぼかした後のバッファの同座標ピクセルの値の差の絶対値をエッジの判定に用いる。一定以上あればエッジであると判断し輪郭として暗くする。絶対値の大きさによって暗くする度合いを調整すればアンチエイリアスのかかった線が引かれる。

以下はその最後のフルスクリーン処理部のHLSLを抜き出しただけのもの。EffectEditへもっていっても動かないよ。ボケているZとボケていないZふたつをサンプルして差分を見ている。

technique filter
{
	pass p0
	{
		VertexShader = compile vs_1_1 vs();
		PixelShader = 
		asm {
		    ps_2_0
		    def c0, 1, -0.07, 0, 14
		    dcl t0.xy
		    dcl_2d s0
		    dcl_2d s1
		    texld r0, t0, s1
		    texld r7, t0, s0
		    add r9, r7, -r0
		    dp4 r4.w, c0.x, r9
		    abs r6.w, r4.w
		    mad r8, r6.w, c0.w, c0.y // c = c * 14 - 0.07
		    mov oC0, r8
		};

		AlphaBlendEnable = true;
		BlendOp = add;
		SrcBlend = zero;
		DestBlend = invsrccolor; // (1-src)を乗算して暗くする
	}
}

結果



所感

  • とりあえず満足した。
  • フルスクリーンのポストエフェクトはやっぱり重い。命令数が少なくてもダメ。ピクセルシェーダを通るピクセルの数を落とす以上効果のある最適化は有り得ない。ビデオカードがヘタレすぎるということもあるけれども。
  • 輪郭線を描く対象物が占める面積がそれほどでもないなら画面全体一括でなくそこだけの大きさの面を描くとかしてもいいかもしれない。
  • 問題は冷静に考えて使えるシーンがあまりないような気がすること。バストアップが多いゲームでないと使いようがないような気がする。動きが多くロングショットで適当に輪郭がかけてりゃいいならお馴染みの陰面膨らまし描画をやっておけばいいんだから。
  • 一世代前のビデオカードには微妙にツライ重さ。