Vertex Shader:モーションブラー


~また、nVIDIAのサンプルかよ~




■はじめに

高速に移動するオブジェクトに後を引くエフェクトをつけるとスピード感が出ます。 ゲームでは、JSRF (Jet Set Radio Future:株式会社SEGA) で、効果的に使われています。
これは、nVIDIA のサンプルの解説です。
いや、ネタ切れというわけでは・・・

今回のソースは、次のものです。

内容は次のとおりになっています。

main.h基本的な定数など。今回も出番無し。
main.cpp描画に関係しないシステム的な部分。変更が無いので、出番無し。
draw.h描画の各関数の定義。特に意味無いので出番無し。
draw.cppメインの描画部分。主にここが説明される。
vs.vshオブジェクト(車)表示用の頂点シェーダープログラム。平行光源ライト。

あと、モデルとして、nsx.xと、実行ファイルの MyBase.exe 及び、 VC++ でコンパイルするためのプロジェクトファイル MyBase.dsw MyBase.dsp が入っています。

■モーションブラーとは

モーションブラーの解説はMasaさんのHPに詳しいので、ここではあっさり流します。
Masa さん曰く、画像がぶれる効果には2種類のものが在るそうです。

Iモーションブラー:フィルムを写すときのシャッターを開く時間は有限であり、高速に移動するオブジェクトはぶれて感光される。
動いた範囲を、一様な半透明のスーパーサンプリング(沢山の回数レンダリング)することにより表現可能。
II残像:網膜に記録された画像が一瞬で消えずに時間とともに減衰していく
目が自動的に行うはずなのでコンピューターで行う必要はない。

今回行う方法は、モデルの一部を引き伸ばして、しかも引き伸ばした先が薄くなるようにレンダリングします。
従って上の立場から言うと、残像を擬似的に表現しているといえます。
まぁ、いろんな場所でモーションブラーとして紹介されているので、そのようにしておきます。

N.B.) BBS で Masa さんから、
> モーションブラー関係で、当時は
> 残像はCGには必要ない」みたいなことを書いたのですが、
> これは本当に「裸眼で見る」と同じような
> 写実」を求めるという意図で書いてました。
>
> ゲームやリアルタイムCGで、
> 良い意味での適度なディフォルメを施す場合、
> 残像は逆に重要なファクタだと思っています。
> #当時はあまりそうは考えてなかったのですけどね(汗
> 今は残像の虜です(謎
の、コメントをいただきました。
Non-Photorealistic Rendering の一つとして、残像はこれから非常に有用なものとなりそうです。

■方法

レンダリングの方法ですが、今回は普通のモデルと引き伸ばされた半透明のモデルの2回を描画しています。
引き伸ばしは、頂点シェーダーで実装しています。

移動量 V と法線 N を比較して、その値が負だったら(進行方向の裏を向いていた場合には)移動する前の座標を頂点に採用します。
今回は、大げさに見えるような演出として、進行方向の裏を向いていた頂点に関して、さらに後ろの頂点 (v0'=v0-3*V) を用いています。

実際のレンダリング順は、Zバッファを上書きしないで半透明な引き伸ばされたモデルを描画した後、普通にモデルを描画します。
後で半透明のモデルを描画すると、頂点が重なった場所の近く(表と裏を向く境界付近)で、Zバッファの誤判定が起きたりして、汚くなる場合があります。
また、半透明のモデルを描画するときにZバッファの書き込みを切っておかないと、通常モデルが後で書き込めなくなります。
但し、深度チェックはしないと、壁の裏にモデルがいる場合に半透明の部分だけ壁の上に描画される現象が起きます。

■シェーダープログラム

では、シェーダープログラムです。
今回は引き伸ばしが起きるので、頂点が単純な透視変換ではありません。

まず、現在と昔(前回)のビュー行列を使って座標をビュー座標系へ変換してからそれらの差分を取って速度ベクトルを算出します。
今回、ワールド座標ではなく、ビュー座標で差分を取ります。
この結果、カメラが移動したときにブラーがかかります。
カメラが移動したときはブラーがいらなければ、ワールド座標系で速度ベクトルを計算すればよいです。
あとは、差分をとって適当に引き伸ばした後、法線(のビュー座標系での表現)と速度ベクトルの内積の正負に応じて、 引き伸ばされた座標を計算します。

; c0     -- {0.0, 0.5, 1.0, 2.0}
; c1     -- メッシュの色
; c2     -- ブラーしたメッシュの色
; c3     -- ブラー用パラメータ (メッシュのサイズ/2.0f, ブラーの長さの倍率, 1, 0)
; c4-7   -- 透視変換行列
; c8-11  -- 現在のビュー座標系
; c12-15 -- 前のビュー座標系
; c16-19 -- ビュー行列 の逆転置行列
;
; v0	頂点の座標値
; v3	法線ベクトル
; v7	テクスチャ座標0

vs.1.0

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 座標変換

; 前のビュー座標系で座標を変換する
dp4 r0.x, v0, c12
dp4 r0.y, v0, c13
dp4 r0.z, v0, c14

; 現在のビュー座標系で座標を変換する
dp4 r1.x, v0, c8
dp4 r1.y, v0, c9
dp4 r1.z, v0, c10

;差を取って、速度ベクトルを求める
add r2.xyz, r1, -r0

; 適当な係数を掛けて、ブラーの長さを調整する
mul r2.xyz, r2, c3.y

; 法線をビュー座標系に変換する
dp3 r3.x, v3, c16
dp3 r3.y, v3, c17
dp3 r3.z, v3, c18

; 速度の法線方向の成分を求める
dp3 r2.w, r2, r3

; (r2.w < 0) ?  r4 = r1-r2 : r4 = r1
slt  r3.w,   r2.w,  c0.x              ; r3.w   = (r2.w < 0.0f) ? 1.0f : 0.0f
mad  r4.xyz, r3.w, -r2, r1            ; r4.xyz = (r2.w < 0.0f) ? r1-r2:r1
mov  r4.w,   c0.z                     ; r4.w   = 1.0f

; r4 を透視変換
dp4 oPos.x, r4, c4
dp4 oPos.y, r4, c5
dp4 oPos.z, r4, c6
dp4 oPos.w, r4, c7

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 速度と法線の関係から色を計算する

dp3 r2.w, r2,  r2
rsq r1.w, r2.w
mul r2.w, r2.w, r1.w                ; r2.w = 速度ベクトルの長さ

mad r2.w, -r2.w, c3.x, c3.z         ; r2.w = 1 - 速度ベクトルの長さ/extent (extent = メッシュのサイズ/2.0f)

mul r5, c2, r2.w                    ; c2' = ブラーのついた基本色(c2) * r2.w

add r5,  r5, - c1
mad oD0, r3.w, r5, c1               ; 色 = c1 + Max(0,N・V)*(c2'-c1)
                                    ;     法線と速度の向きが同じなら color(ブラーの色として設定された色)
                                    ;     法線と速度の向き反対なら メッシュの色

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; テクスチャーを張る
mov oT0.xy, v7

頂点色もいじっています。
移動速度を求めて、その長さに応じて、色を薄くします。
今回は、移動量がモデルのサイズの 1/2 になったら切れる程度の長さにしています。
最後に頂点と同じように速度方向を向いているかどうかで普通の色か、ブラーのかかった薄い色を選択します。

テクスチャーは普段どおりに張ります。

■Cソース

最後に描画部分です。
いつも通りワールド行列やビュー、射影行列を計算した後、レジスタを設定します。
今回は同心円状に移動するワールド行列を作りました。
MeshRadius はモデルの大きさです。初期化の時に MeshRadius = max(sqrt(x^2+y^2+z^2))で導出しています。
また、この MeshRadius は、ブラーした色の半透明度を決定する定数レジスタ c3 にも使っています。

void Render(LPDIRECT3DDEVICE8 lpD3DDev)
{
    if(NULL == pMeshVB) return;
    
    D3DXMATRIX m, mWorld, mView, mProj;
    DWORD i;

    // ワールド行列
    D3DXMatrixTranslation(&m, MeshRadius, 0,0);
    D3DXMatrixRotationY( &mWorld, timeGetTime()/1000.0f );
    mWorld = m * mWorld;
    
    // ビュー行列
    D3DXVECTOR3 eye   (0.0f,MeshRadius,2.5f*MeshRadius);
    D3DXVECTOR3 lookAt(0.0f, 0.0f, 0.0f);
    D3DXVECTOR3 up    (0.0f, 1.0f, 0.0f);
    D3DXMatrixLookAtLH(&mView, &eye, &lookAt, &up);
    // 射影行列
    D3DXMatrixPerspectiveFovLH(&mProj, PI/3.0f, (float)WIDTH/(float)HEIGHT, 0.01f, 100.0f);
    
    // パラメータの設定
    lpD3DDev->SetRenderState( D3DRS_ZENABLE,        TRUE );
    
    lpD3DDev->SetRenderState( D3DRS_ALPHABLENDENABLE, TRUE);      // 半透明合成は内挿合成
    lpD3DDev->SetRenderState( D3DRS_SRCBLEND,  D3DBLEND_SRCALPHA );
    lpD3DDev->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);

    lpD3DDev->SetVertexShaderConstant(0, D3DXVECTOR4(0.0f,0.5f,1.0f,2.0f), 1);
    lpD3DDev->SetVertexShaderConstant( 3, D3DXVECTOR4(
        2.0f/MeshRadius,
        3.0f,                // ブラーの長さ
        1.0f,0.0f),  1 );
    
    lpD3DDev->SetVertexShader(hVertexShader);
    lpD3DDev->SetStreamSource(0, pMeshVB, sizeof(D3D_CUSTOMVERTEX));
    lpD3DDev->SetIndices(pMeshIndex,0);
    
    //メッシュの描画(透明なブラー部分)
    set_transform(1,lpD3DDev, mWorld, mView, mProj);
    lpD3DDev->SetRenderState( D3DRS_ZWRITEENABLE, FALSE );    // 深度バッファの書き込みを止める
    for(i=0; i < dwNumMaterials; i++){
        //色をセット
        D3DXVECTOR4 c(pMeshMaterials[i].Diffuse.r,
                      pMeshMaterials[i].Diffuse.g,
                      pMeshMaterials[i].Diffuse.b,
                      1.0f);
        FLOAT const kBlurAlpha = 0.0f;

        lpD3DDev->SetVertexShaderConstant( 1, &c,  1 );  // メッシュの色
        c[3] = kBlurAlpha;
        lpD3DDev->SetVertexShaderConstant( 2, &c,  1 );  // ブラーしたメッシュの色

        lpD3DDev->SetTexture(0, pMeshTextures[i]);
        lpD3DDev->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 
                                        pSubsetTable[i].VertexStart,
                                        pSubsetTable[i].VertexCount,
                                        pSubsetTable[i].FaceStart * 3,
                                        pSubsetTable[i].FaceCount);
    }
    
    //メッシュの描画(2パス目:通常描画)
    set_transform(0,lpD3DDev, mWorld, mView, mProj);
    lpD3DDev->SetRenderState( D3DRS_ZWRITEENABLE, TRUE );    // 深度バッファへ書き込む
    for(i=0; i < dwNumMaterials;i++){
        //色をセット
        D3DXVECTOR4 c(pMeshMaterials[i].Diffuse.r,
                      pMeshMaterials[i].Diffuse.g,
                      pMeshMaterials[i].Diffuse.b,
                      1.0f);
        lpD3DDev->SetVertexShaderConstant( 1, &c,  1 );  // メッシュの色
        lpD3DDev->SetVertexShaderConstant( 2, &c,  1 );  // ブラーしたメッシュの色

        lpD3DDev->SetTexture(0, pMeshTextures[i]);
        lpD3DDev->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 
                                        pSubsetTable[i].VertexStart,
                                        pSubsetTable[i].VertexCount,
                                        pSubsetTable[i].FaceStart * 3,
                                        pSubsetTable[i].FaceCount);
    }
    
    Sleep(100); // 残像が残るための時間を確保します
}

2回の描画部分を色分けしています。
set_transform で行列を設定していますが、これは後で。
Z バッファの書き込みを切り替えてから、いつもの様に描画します。
今回は色を設定するレジスタが2つあります。ブラーに使う色はα成分を0にしています。
頂点シェーダーで減衰しているので、必要なかったりもしますが、色が切れて消えるのがいやだったので、α成分で制御するようにもしました。

一度描画した後(十分に尾が伸びる時間を待つため)、0.1 秒ほど待っています。

通常描画部分ですが、同じ頂点シェーダープログラムを使っています。
違いは前のフレームの頂点を使うところで、現在の頂点を使うようにします。また、色も現在の色と前の色も同じにして、普通のレンダリングを実現します。
違いは set_transform で吸収しています。

static set_transform(bool bWithBlur, LPDIRECT3DDEVICE8 lpD3DDev, D3DXMATRIX mWorld, D3DXMATRIX mView, D3DXMATRIX mProj)
{
    D3DXMATRIX m, mWV, mWV_IT;
    static D3DXMATRIX mLastWV;

    D3DXMatrixMultiply(&mWV,   &mWorld, &mView);
    D3DXMatrixInverse( &mWV_IT, NULL, &mWV);
    
    // 射影行列
    D3DXMatrixTranspose(&m, &mProj);
    lpD3DDev->SetVertexShaderConstant(4, &m, 4);

    // ビュー行列
    D3DXMatrixTranspose(&mWV, &mWV);
    lpD3DDev->SetVertexShaderConstant(8, &mWV, 3);
    if (bWithBlur){
        lpD3DDev->SetVertexShaderConstant(12, &mLastWV, 3);
        mLastWV = mWV;
    }else{
        // ブラーしないときは、現在のマトリックスを過去のものとして使用
        lpD3DDev->SetVertexShaderConstant(12, &mWV, 3);
    }

    // ビュー行列 の逆転置行列
    lpD3DDev->SetVertexShaderConstant(16, &mWV_IT, 3);

    return S_OK;
}

set_transform では、最初の引数 bWithBlur でブラーの描画か通常の描画かを判定しています。
ブラーがかかる場合には、前回の行列を保存する静的変数 mLastWV に今回の行列を保存します。
あとは、必要な行列を設定します。

■最後に

今回は、頂点の引き伸ばしによるモーションブラーの表現をしました。
頂点をいじる演出は少ないですね(後は、輪郭抽出しか浮かびません)。ひょっとしてまだ発見されていない面白いエフェクトがあるかも。




もどる

imagire@gmail.com