深度法線エッジによる輪郭抽出


~再び輪郭抽出~




■はじめに

エッジフィルターを利用した輪郭抽出です。
ATIの論文によく出てるやつです。
今回は4パスも使ってます。

まぁ、いつものように適当にファイルが入っています。

normal.vsh法線マップ生成頂点シェーダー。
normal.psh法線マップ生成ピクセルシェーダー。
normaledge.vsh法線マップのエッジ生成頂点シェーダー。
normaledge.psh法線マップのエッジ生成ピクセルシェーダー。
vs.vsh深度、通常色の頂点シェーダー。
ps.psh深度、通常色のピクセルシェーダー。
final.vsh深度エッジの頂点シェーダー。
final.psh深度エッジのピクセルシェーダー。
draw.cppメインの描画部分。
draw.h描画の各関数の定義。
bg.cpp背景の描画。
main.h基本的な定数など。
main.cpp描画に関係しないシステム的な部分。
load.cppロード。
load.hロードのインターフェイス。
tile.bmp (床デカール)
sky.bmp (空デカール)

あと、実行ファイル及び、プロジェクトファイルが入っています。

■方法

作成した画像からエッジを作成するのに、法線に関するエッジを取ればよさそうですが、それだけでは、ほぼ同じ法線を持った面が重なった時に エッジが抽出できないので、深度に関するエッジも取る必要があることがよく知られています。
最近のプログラマブルシェーダーの力はそれらの処理を楽勝で行えます。
じゃぁ、どうやってやるのかという話になります。

最初に、法線マップによるエッジを抽出します。
1パス使って、法線マップのテクスチャーを作ります。
2パス目で、フレームバッファに法線マップのエッジを作成します。

次のパスでは、法線マップのことはすっかり忘れます。
3パス目で、テクスチャーに通常のレンダリングと、アルファ成分に深度情報を埋め込みます。
4パス目で、深度エッジを作成し、テクスチャーの色と積算して合成します。(前のスクリーン全体に関するエッジフィルターを深度値で行った様にです)。
最後に、フレームバッファへの合成時のアルファ合成を使って、乗算合成で深度エッジと乗算し、最終的なエッジを得ます。

■シェーダープログラム

さて、どのようにして、以上の手続きを実現するか、ピクセルシェーダーのプログラムを見てみましょう。

最初は法線マップの作成です。
頂点シェーダーでは、座標変換を普通に行った後、-1から1に分布している法線の値を0~1になるよう、oD0=n/2+1/2 の計算をして色に出力します。
ピクセルシェーダーでは、色をそのまま出力します。

0001: ;normal.vsh
0002: ; c0-3   -- world + ビュー + 透視変換行列
0003: ; c12    -- (0.0, 1.0, 0.5, 2.0)
0004: ;
0005: ; v0    頂点の座標値
0006: ; v3    法線ベクトル
0009: vs.1.0
0011: ;座標変換
0012: m4x4 oPos, v0, c0
0014: ; 法線ベクトルを色で格納
0015: mad oD0,   v3,   c12.y, c12.y
0001: ; normal.psh
0002: ;
0004: ps.1.0
0006: mov r0, v0

法線エッジの抽出は、テクスチャー座標を微妙にずらした4枚のテクスチャーを用意します。
ピクセルシェーダーで対角的な差分を各色成分ごと取り、成分の2乗和をとって、適当に倍数を掛けます。

0001: ; normaledge.vsh
0002: ; c20-23   -- テクスチャーをずらすオフセット
0003: ;             (0,0), (1,0), (0,1), (1,1)
0004: ; v0    頂点の座標値
0005: ; v7    テクスチャ座標
0006: 
0007: vs.1.0
0008: 
0009: ; 座標を代入
0010: mov oPos, v0
0011: 
0012: ; それぞれテクスチャーの位置をずらして出力
0013: add oT0, v7, c20
0014: add oT1, v7, c21
0015: add oT2, v7, c22
0016: add oT3, v7, c23
0001: ; normaledge.psh
0004: ps.1.0
0005: 
0006: tex t0      ; 0:1 0  1:0 1  2:0 0  3:0 0
0007: tex t1      ;   0 0    0 0    1 0    0 1
0008: tex t2
0009: tex t3
0010: 
0011: mov        r0, t3
0012: mov        r1, t2
0013: add_x4     r0,  r0, -t0 ; r0 =  4(t3-t0)
0014: add_x4     r1,  r1, -t1 ; r1 =  4(t2-t1)
0015: 
0016: dp3_x4     r0,  r0,  r0 ; r0 = 64|t3-t0|^2
0017: dp3_x4     r1,  r1,  r1 ; r1 = 64|t2-t1|^2
0018: add_x4_sat r0, 1-r0,-r1 ; r0 = 4(1-64(|t3-t0|^2+|t2-t1|^2))

次は、深度マップの作り方です。
今回は、「手前の解像度が相対的に大事」ということで、手前の分解能を上げる変換を使いました(いわゆる視点からの相対深度)。

    Zfar(Z   -Znear)
z = ― ―――――
    Z   (Zfar-Znear)

後は、普通にディフューズの計算です。
ピクセルシェーダーでは、色成分とアルファ成分を合成する「+」を使って、ちょっとだけ高速化を考えています。

0001: ;vs.vsh
0002: ; c0-3   -- world + ビュー + 透視変換行列
0003: ; c15    -- 深度調整
0004: ;
0005: ; v0    頂点の座標値
0006: ; v7    テクスチャ座標
0007: 
0008: vs.1.0
0009: 
0010: ; デカールテクスチャー
0011: mov oT0,    v7
0012: 
0013: ;座標変換
0014: m4x4 oPos, v0, c0
0015: 
0016: ; 深度を色のw成分に入れる
0017: dp4 r0.z,   v0,   c2
0018: dp4 r0.w,   v0,   c3
0019: mad r0.z,   r0.z, c15.x, c15.y
0020: rcp r0.w,   r0.w
0021: mul oD0,    r0.z, r0.w
0022: 
0023: 
0024: ; 色をつける
0025: dp4 r0.w,   v3,   c13
0026: mul oD0.xyz,r0.w, c14
0001: ; ps.psh
0003: 
0004: ps.1.0
0005: 
0006: tex t0
0007: 
0008: add r0.rgb, v0, t0
0009: +mov r0.a, v0

深度エッジは、法線エッジを求める時とほとんど変わりません。
違いは、テクスチャーのアルファ成分の差分を計算して、最後に、エッジの結果と色を掛け合わせることです。

0001: ; final.vsh
0002: ; c20-23   -- テクスチャーをずらすオフセット
0003: ;             (0,0), (1,0), (0,1), (1,1)
0004: ; v0    頂点の座標値
0005: ; v7    テクスチャ座標
0006: 
0007: vs.1.0
0008: 
0009: ; 座標を代入
0010: mov oPos, v0
0011: 
0012: ; それぞれテクスチャーの位置をずらして出力
0013: add oT0, v7, c20
0014: add oT1, v7, c21
0015: add oT2, v7, c22
0016: add oT3, v7, c23
0001: ; final.psh
0004: ps.1.0
0005: 
0006: tex t0      ; 0:1 0  1:0 1  2:0 0  3:0 0
0007: tex t1      ;   0 0    0 0    1 0    0 1
0008: tex t2
0009: tex t3
0010: 
0011: mov         r0, t3.a
0012: mov         r1, t2.a
0013: add_x4      r0, r0, -t0.a   ; r0 =  4(t3.a-t0.a)
0014: add_x4      r1, r1, -t1.a   ; r1 =  4(t2.a-t1.a)
0015: mul_x4      r0, r0, r0      ; r0 = 64(t3.a-t0.a)^2
0016: mul_x4      r1, r1, r1      ; r1 = 64(t2.a-t1.a)^2
0017: add_x4_sat  r0, 1-r0.a,-r1.a; r0 = 4(1-64((t3.a-t0.a)^2+(t2.a-t1.a)^2))
0018: mul_sat     r0, r0.a, t1    ; r0のエッジに色を乗せる

■cソース

呼び出しているcプログラムですが、ほとんど同じループが2回繰り返されています(手を抜いてコピー&ペーストしたってばらしちゃ駄目です)。
違いは、シェーダープログラムと、アルファ合成に関する SetRenderState の追加です。
この設定によって、最終的に法線エッジとそれ以外の深度エッジと通常のレンダリングの結果を合成します。

draw.cpp
0423: VOID Render(LPDIRECT3DDEVICE8 lpD3DDev)
0424: {
0425:     DWORD i;
0426: 
0427:     LPDIRECT3DSURFACE8 lpZbuffer = NULL;
0428: 
0429:     lpD3DDev->GetDepthStencilSurface( &lpZbuffer );
0430:     // -----------------------------------------------------
0431:     // 法線エッジの作成
0432:     // -----------------------------------------------------
0433:     // テクスチャーに描画
0434:     lpD3DDev->SetRenderTarget(pTextureSurface, lpZbuffer);
0435:     lpD3DDev->SetTextureStageState(0,D3DTSS_COLOROP,    D3DTOP_SELECTARG1);
0436:     lpD3DDev->SetTextureStageState(0,D3DTSS_COLORARG1,  D3DTA_DIFFUSE);
0437:     lpD3DDev->SetTextureStageState(1,D3DTSS_COLOROP,    D3DTOP_DISABLE);
0438:     lpD3DDev->SetVertexShader(hVertexShaderNormal);
0439:     lpD3DDev->SetPixelShader(hPixelShaderNormal);
0440:     DrawModel(lpD3DDev);
0441: 
0442:     // 描画をバックバッファに戻す
0443:     lpD3DDev->SetRenderTarget(pBackbuffer, lpZbuffer );
0444:     for (i = 0; i < 4; i++) {
0445:         lpD3DDev->SetTextureStageState(i, D3DTSS_ADDRESSU,  D3DTADDRESS_CLAMP);
0446:         lpD3DDev->SetTextureStageState(i, D3DTSS_ADDRESSV,  D3DTADDRESS_CLAMP);
0447:         lpD3DDev->SetTextureStageState(i, D3DTSS_COLOROP,   D3DTOP_SELECTARG1);
0448:         lpD3DDev->SetTextureStageState(i, D3DTSS_COLORARG1, D3DTA_TEXTURE);
0449:         lpD3DDev->SetTexture( i, pTexture );         // テクスチャー
0450:     }
0451: 
0452:     lpD3DDev->SetVertexShader(hVertexShaderNormalEdge);
0453:     lpD3DDev->SetPixelShader(hPixelShaderNormalEdge);
0454:     lpD3DDev->SetStreamSource( 0, pFinalVB, sizeof(D3D_FINAL_VERTEX) );
0455:     lpD3DDev->SetIndices(pFinalIB,0);
0456:     lpD3DDev->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 4, 0, 2 );
0457:     
0458:     lpD3DDev->SetTexture( 0, NULL);
0459:     lpD3DDev->SetTexture( 1, NULL);
0460:     lpD3DDev->SetTexture( 2, NULL);
0461:     lpD3DDev->SetTexture( 3, NULL);
0462: 
0463:     // -----------------------------------------------------
0464:     // 深度エッジと色の作成
0465:     // -----------------------------------------------------
0466:     // テクスチャーに描画
0467:     lpD3DDev->SetRenderTarget(pTextureSurface, lpZbuffer);
0468:     lpD3DDev->Clear(0,NULL, D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0,0,0),1.0f,0);
0469:     lpD3DDev->SetTextureStageState(0,D3DTSS_COLOROP,    D3DTOP_MODULATE);
0470:     lpD3DDev->SetTextureStageState(0,D3DTSS_COLORARG1,  D3DTA_TEXTURE);
0471:     lpD3DDev->SetTextureStageState(0,D3DTSS_COLORARG2,  D3DTA_DIFFUSE);
0472:     lpD3DDev->SetTextureStageState(1,D3DTSS_COLOROP,    D3DTOP_DISABLE);
0473:     lpD3DDev->SetVertexShader(hVertexShader);
0474:     lpD3DDev->SetPixelShader(hPixelShader);
0475:     DrawModel(lpD3DDev);
0476: 
0477:     // 描画をバックバッファに戻す
0478:     lpD3DDev->SetRenderTarget(pBackbuffer, lpZbuffer );
0479:     for (i = 0; i < 4; i++) {
0480:         lpD3DDev->SetTextureStageState(i, D3DTSS_ADDRESSU,  D3DTADDRESS_CLAMP);
0481:         lpD3DDev->SetTextureStageState(i, D3DTSS_ADDRESSV,  D3DTADDRESS_CLAMP);
0482:         lpD3DDev->SetTextureStageState(i, D3DTSS_COLOROP,   D3DTOP_SELECTARG1);
0483:         lpD3DDev->SetTextureStageState(i, D3DTSS_COLORARG1, D3DTA_TEXTURE);
0484:         lpD3DDev->SetTexture( i, pTexture );            // テクスチャー
0485:     }
0486:     lpD3DDev->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ZERO);
0487:     lpD3DDev->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_DESTCOLOR);
0488:     lpD3DDev->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
0489: 
0490:     lpD3DDev->SetVertexShader(hFinalVertexShader);
0491:     lpD3DDev->SetPixelShader(hFinalPixelShader);
0492:     lpD3DDev->SetStreamSource( 0, pFinalVB, sizeof(D3D_FINAL_VERTEX) );
0493:     lpD3DDev->SetIndices(pFinalIB,0);
0494:     lpD3DDev->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 4, 0, 2 );
0495: 
0496:     lpD3DDev->SetPixelShader(0);
0497:     lpD3DDev->SetTexture( 0, NULL);
0498:     lpD3DDev->SetTexture( 1, NULL);
0499:     lpD3DDev->SetTexture( 2, NULL);
0500:     lpD3DDev->SetTexture( 3, NULL);
0501:     lpD3DDev->SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE);
0529: }

■最後に

ほぼ一年くらい前から、『テクスチャーによる陰影を利用した輪郭抽出』、『裏向き外側膨らませモデル描画による輪郭抽出』、 『フルスクリーンエッジフィルターによる輪郭抽出』と、今回の『深度、法線マップによる輪郭抽出』をやってきましたが、 一年ぐらいでずいぶん進化しましたね。
このペースで進化しつづけるとすると、本当に映画のクオリティがリアルタイムにできるようになりそうです。

■追記:IDマップによるエッジの追加

掲示板で、新坂さんから、「マテリアル境界に線を引くのは必須ですね」言われました。
また、「デプス、法線、マテリアルIDの3つの中で単体で一番まともに使えるのがマテリアルIDを使った方法だと思います。」と、豪語されてしまいました。
じゃぁ、やらないわけには行かないじゃないですか。
といっても、今回の画像では、エッジを引かれた部分の色が濃いのでエッジが目立ちませんね。

ファイル構成は同じです。

normal.vsh法線マップ生成頂点シェーダー。
normal.psh法線マップ生成ピクセルシェーダー。
normaledge.vsh法線マップのエッジ生成頂点シェーダー。
normaledge.psh法線マップのエッジ生成ピクセルシェーダー。
vs.vsh深度、通常色の頂点シェーダー。
ps.psh深度、通常色のピクセルシェーダー。
final.vsh深度エッジの頂点シェーダー。
final.psh深度エッジのピクセルシェーダー。
draw.cppメインの描画部分。
draw.h描画の各関数の定義。
bg.cpp背景の描画。
main.h基本的な定数など。
main.cpp描画に関係しないシステム的な部分。
load.cppロード。
load.hロードのインターフェイス。

今回の方法は、DrawIndexedPrimitiveの単位でIDを付けて、IDマップを作ります。

エッジを取って、

法線マップのエッジ

と合成して、IDと法線のエッジを1パスで作ってから、後は同じように合成します。

法線マップとIDマップの違いはマテリアルの境界にくっきりとエッジが出来るので、 アニメっぽい線が引けることです。

具体的な実装方法をしては、IDを振るクラスを作りました。

draw.h
0015: class IdMgr{
0016:     static float id;
0017: public:
0018:     static void Reset(){id=0.0f;}
0019:     static float GetId() {return id+=32.0f/256.0f;}
0020: };

Reset で数値をクリアして、GetId()で数値を得ると共に内部の数値を進めます。
GetId()をみて分かるとおり、8段階しかIDを触れないので、今回は車を描画するごとに数値をResetして、 車の同じ部位に関しては同じ値になるようにしました。
ID間の値の差が小さいと、エッジを取ったときにエッジが消える可能性があるので、適当に大きな差が必要になります。
車ごとのエッジは深度エッジで取れるので、これで十分です。
これをDrawIndexedPrimitiveごとに、定数レジスタに設定しました。

draw.cpp
0407:     IdMgr::Reset();
0408:     for(i=0;i<dwNumMaterials;i++){

色の設定。略。

0414:         lpD3DDev->SetVertexShaderConstant(16, &D3DXVECTOR4(0,0,0,IdMgr::GetId()), 1);
0415: 
0416:         lpD3DDev->SetMaterial( &pMeshMaterials[i] );
0417:         lpD3DDev->SetTexture(0,pMeshTextures[i]);
0418:         lpD3DDev->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 
0419:                                         pSubsetTable[i].VertexStart,
0420:                                         pSubsetTable[i].VertexCount,
0421:                                         pSubsetTable[i].FaceStart * 3,
0422:                                         pSubsetTable[i].FaceCount);
0423:     }

頂点レジスタでは、色のα成分にIDマップを作ります。

0001: ;normal.vsh
0002: ; c0-3   -- world + ビュー + 透視変換行列
0003: ; c12    -- (0.0, 1.0, 0.5, 2.0)
0004: ; c16    -- ID を示した色
0005: ;
0006: ; v0    頂点の座標値
0007: ; v3    法線ベクトル
0008: ; v7    テクスチャ座標
0009: 
0010: vs.1.0
0011: 
0012: ;座標変換
0013: m4x4 oPos, v0, c0
0014: 
0015: ; 法線ベクトルを色で格納
0016: mad oD0,   v3,   c12.y, c12.y
0017: mov oD0.w, c16

あとは、エッジを作るピクセルシェーダーが異なります。
今回は、ps1.1 です。ps1.1は、一つの命令にテクスチャーレジスタが1つしか使えないという制限が無く、 rレジスタと同じ感覚で使えるので、とても便利ですと、新坂さんに教えていただきました。α成分にIDを入れるのも新坂さんに教えていただいた方法です。本当に新坂さまさまさまです
ps1.xの少ない命令数を補うために、+命令を使って、RGB成分とα成分を同時に別の命令を実行して命令数を稼いでいます。
内容的には、α成分の差の2乗を計算して、法線エッジのエッジに足しこんでいます。

0001: ; normaledge.psh
0002: ;      (c) 2002 IMAGIRE Takashi
0003: 
0004: ps.1.1
0005: 
0006: tex t0      ; 0:1 0  1:0 1  2:0 0  3:0 0
0007: tex t1      ;   0 0    0 0    1 0    0 1
0008: tex t2
0009: tex t3
0010: 
0011: add_x4     r0,  t3, -t0 ; r0 =  4(t3-t0)
0012: add_x4     r1,  t2, -t1 ; r1 =  4(t2-t1)
0013: 
0014:  dp3_x4    r0.rgb,  r0,  r0     ; r0 = 64|t3-t0|^2 (rgb)
0015: +mul_x4    r0.a,    r0,  r0     ; r0 = 64|t3-t0|^2 (a)
0016:  dp3_x4    r1.rgb,  r1,  r1     ; r1 = 64|t2-t1|^2 (rgb)
0017: +mul_x4    r1.a,    r1,  r1     ; r1 = 64|t2-t1|^2 (a)
0018: add        r0,      r0,  r0.a   ; r0 = 64|t3-t0|^2 (rgba)
0019: add        r1,      r1,  r1.a   ; r1 = 64|t2-t1|^2 (rgba)
0020: add_x4_sat r0,      1-r0,-r1    ; r0 = 4(1-64(|t3-t0|^2+|t2-t1|^2))

以上が変更点です。これで、IDマップを使ったエッジが作れます。
今回は、DrawIndexedPrimitive単位でIDを振りましたが、テクスチャーに入れてデザイナーの自由に設定をすることも出来ます。 (当然お分かりかとは思いますが、直接線をデカールとして引くこととの違いはポリゴンが傾いたときでも一定の太さの線が引けるということです)。
今回やってみた結果、IDマップは線が綺麗に出ますね。大変使えそうです。




もどる

imagire@gmail.com