トゥーンシェーダー


~はやりもんだしね~




■はじめに

最近のテレビアニメでは、いかにもポリゴンモデルを使ったものが増えてきました。 それらのモデルは、周りのセル画となじむように特殊な処理しています。 トゥーンレンダリングと呼ばれるものです。
今回は、それらを DirectX で表現するトゥーンシェーダーを行います。

一般に、トゥーンレンダリングと呼ばれるものは2つの効果があります。

I段階的な色分け:2、3階調の色で、陰影をつける
II輪郭抽出:全体の外側に黒い枠線がつく

です。
今回は、それらのうち、『段階的な色分け』をします。

いつも通り、以下のファイルをダウンロードしてください。
今回は、toon.bmp というファイルが入っています。これがどのような段階的な塗り分けをするか制御します。

■バーテックスシェーダー(vs.vsh)

さて、いきなり核心部分のバーテックスシェーダープログラムを見てみましょう。
次のようになっています。

vs.1.0

; c0-3   -- world + ビュー + 透視変換行列
; c8-11  -- world の逆転置行列
; c12    -- {0.0, 0.5, 1.0, -1.0}
; c13    -- ライトのベクトル
; c15    -- メッシュの色
;
; v0	頂点の座標値
; v3	法線ベクトル
; v7	テクスチャ座標0
; v8	テクスチャ座標1

;座標変換
dp4 oPos.x, v0,  c0
dp4 oPos.y, v0,  c1
dp4 oPos.z, v0,  c2
dp4 oPos.w, v0,  c3

;法線の変換
dp3 r0.x,   v3,  c8
dp3 r0.y,   v3,  c9
dp3 r0.z,   v3,  c10

;法線の正規化
dp3 r0.w,   r0,  r0
rsq r0.w,   r0.w
mul r0,     r0,  r0.w

; l dot n (ライティング)
dp3 r0,     r0,  c13
add r0,     r0,  c12.z ; r0=r0+1.0f (0<=r0<=2.0f)
mul oT0.x,  r0,  c12.y ; r0=r0*0.5f (0<=r0<=1.0f)

; メッシュのテクスチャー
mov oT1,    v8

; メッシュの色
mov oD0,    c15

最初の座標変換は、すでに出てきた、オブジェクトを画面での位置に変換します。
次に、ライティングを行います。ここが、今回のミソです。
前回は、ライトベクトルにワールド行列の逆行列を掛けたベクトルを固定レジスタに代入しましたが、 今回は、それをバーテックスシェーダープログラム内で、計算しています。
ただし、その計算を法線に適用しています。正規化は、rsq命令 (1/Sqrt(r0.w)を計算する)でします。
今回は、それを oD0 に入れません。ライトと法線よって求められた結果は oT0.x つまり、テクスチャーの U に入れます。
このテクスチャーに用いるのが、toon.bmp です。これは、

の形をしています。
oT0.x は横軸の値なので、内積の値が大きければ白く(明るく)、小さければ暗く(黒く)なります。
それが、段階的になっているので、合成したときに段階的な色合いが表現されます。
実際には、このテクスチャーでだけではなくて、xfile で設定されたテクスチャーも張ります。
その二つが合成されて、陰影のついたレンダリングが得られます。 ちなみに、toon.bmp を下のような連続的なテクスチャーにした場合には、普通のライティングと同じ結果が得られます。

oD0 には、xfile から読み取ったメッシュの色をそのまま入れます(最後の行)。
また、oT1 に xfile のテクスチャーをそのまま張ります。

■オブジェクト

さて、draw.cpp を見ます。
今回のオブジェクトは、多いです。

LPDIRECT3DVERTEXBUFFER8    pMeshVB = NULL;
LPDIRECT3DINDEXBUFFER8     pMeshIndex = NULL;
D3DXATTRIBUTERANGE        *pSubsetTable = NULL;
DWORD                      nMeshFaces = 0;
DWORD                      nMeshVertices = 0;
D3DMATERIAL8              *pMeshMaterials = NULL;        // メッシュの質感
LPDIRECT3DTEXTURE8        *pMeshTextures  = NULL;        // メッシュのテクスチャー
DWORD                      dwNumMaterials = 0L;        // マテリアルの数

LPDIRECT3DTEXTURE8         pTexture  = NULL;            // エフェクト用のテクスチャー
DWORD                      hVertexShader=~0;

前半は、xfile 用のオブジェクトです。Index buffer を使っています。
参考に見たプログラムが使っていたからという理由は秘密です。
pTexture は toon.bmp、hVertexShader は vs.vsh のオブジェクトです。

■頂点

今回は2つ使います。ひとつ (D3DVERTEX) は、xfile に入っている情報で、 もうひとつ (D3D_CUSTOMVERTEX) は、バーテックスシェーダーで取り扱うフォーマットです。
バーテックスシェーダーでは、普通のテクスチャーと、トゥーンシェーディング用のテクスチャーの2つを使うので、 テクスチャーを2つ持つフォーマットを使います。
それぞれ、D3DVSDE_??? が定義されていましたので、使いました。
v0, v3, v7, v8 で使っている情報は、この D3D_CUSTOMVERTEX のものです。

// xfile に収められた情報
typedef struct {
    float x,y,z;
    float nx,ny,nz;
    float tu0,tv0;
}D3DVERTEX;
#define D3DFVF_VERTEX         (D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_TEX1)

// バーテックスシェーダーのフォーマット
typedef struct {
    float x,y,z;
    float nx,ny,nz;
    float tu0,tv0;
    float tu1,tv1;
}D3D_CUSTOMVERTEX;
#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_TEX1 | D3DFVF_TEX2)

DWORD dwDecl[] = {
    D3DVSD_STREAM(0),
    D3DVSD_REG(D3DVSDE_POSITION, D3DVSDT_FLOAT3 ),            //D3DVSDE_POSITION,  0
    D3DVSD_REG(D3DVSDE_NORMAL,   D3DVSDT_FLOAT3 ),            //D3DVSDE_NORMAL,    3
    D3DVSD_REG(D3DVSDE_TEXCOORD0,D3DVSDT_FLOAT2 ),            //D3DVSDE_TEXCOORD0, 7  
    D3DVSD_REG(D3DVSDE_TEXCOORD1,D3DVSDT_FLOAT2 ),     //D3DVSDE_TEXCOORD1, 8  
    D3DVSD_END()
};

■初期化

では、初期化です。
xfile の部分は長いので、LoadXFile という別関数にしました。
あとは、すでに出てきた、テクスチャーの読み込みと、バーテックスシェーダーの生成です。
また、Zバッファーを使って、背景色も設定しておきます。

HRESULT InitRender(LPDIRECT3DDEVICE8 lpD3DDEV)
{
    HRESULT hr;

    hr = LoadXFile("car.x", lpD3DDEV);
    if ( FAILED(hr) ) return hr;
 
    // 虹色テクスチャーの読み込み
    D3DXCreateTextureFromFileEx(lpD3DDEV, "toon.bmp",0,0,0,0,D3DFMT_A8R8G8B8,
                                D3DPOOL_MANAGED, D3DX_FILTER_LINEAR, D3DX_FILTER_LINEAR,
                                0, NULL, NULL, &pTexture);

    // バーテックスシェーダーを作成する
    ID3DXBuffer*    pshader;
    hr = D3DXAssembleShaderFromFile("vs.vsh", 0,NULL,&pshader,NULL);
    if ( FAILED(hr) ) return hr;
    hr = lpD3DDEV->CreateVertexShader( dwDecl, (DWORD*)pshader->GetBufferPointer(), &hVertexShader, 0 );
    RELEASE(pshader);
    if ( FAILED(hr) ) return hr;
    
    // レンダリングの状態の設定
    lpD3DDEV->SetRenderState( D3DRS_ZENABLE, TRUE);
    lpD3DDEV->SetRenderState( D3DRS_AMBIENT, 0xff808080);
    
    return S_OK;
}

で、肝心の xfile の初期化です。

HRESULT LoadXFile(char* filename, LPDIRECT3DDEVICE8 lpD3DDEV)
{
    LPD3DXMESH pMesh, pMeshOpt;
    LPD3DXBUFFER pD3DXMtrlBuffer = NULL;
    DWORD i;
    HRESULT hr;

    hr = D3DXLoadMeshFromX(filename, D3DXMESH_MANAGED,
                                lpD3DDEV, NULL,
                                &pD3DXMtrlBuffer, &dwNumMaterials,
                                &pMesh);
    if(FAILED(hr)) return E_FAIL;

    // メッシュの面および頂点の順番変更を制御し、パフォーマンスを最適化
    pMesh->Optimize(D3DXMESHOPT_ATTRSORT, NULL, NULL, NULL, NULL, &pMeshOpt);
    RELEASE(pMesh);

    //アトリビュートテーブル
    pMeshOpt->GetAttributeTable(NULL,&dwNumMaterials);
    pSubsetTable = new D3DXATTRIBUTERANGE[dwNumMaterials];
    pMeshOpt->GetAttributeTable(pSubsetTable, &dwNumMaterials);

    // 柔軟な頂点フォーマット (FVF) コードを使用してメッシュのコピーを作成する
    hr = pMeshOpt->CloneMeshFVF(pMeshOpt->GetOptions(), D3DFVF_VERTEX, lpD3DDEV, &pMesh);
    if(FAILED(hr)) return E_FAIL;
    RELEASE(pMeshOpt);
    D3DXComputeNormals(pMesh);

    //Vertex Bufferにコピーする
    D3DVERTEX* pSrc;
    D3D_CUSTOMVERTEX* pDest;
    LPDIRECT3DINDEXBUFFER8 pSrcIndex;
    WORD* pISrc;
    WORD* pIDest;

    DWORD nMeshVertices = pMesh->GetNumVertices();
    DWORD nMeshFaces = pMesh->GetNumFaces();
    lpD3DDEV->CreateVertexBuffer(nMeshVertices * sizeof(D3D_CUSTOMVERTEX),0,D3DFVF_CUSTOMVERTEX,D3DPOOL_MANAGED,&pMeshVB);
    lpD3DDEV->CreateIndexBuffer(nMeshFaces * 3 * sizeof(WORD),0,D3DFMT_INDEX16,D3DPOOL_MANAGED,&pMeshIndex);

    LPDIRECT3DVERTEXBUFFER8 pVB;
    pMesh->GetVertexBuffer(&pVB);
    pVB->Lock(0,0,(BYTE**)&pSrc,0);
    pMeshVB->Lock(0,0,(BYTE**)&pDest,0);
    for(i=0;ix = pSrc->x;
        pDest->y = pSrc->y;
        pDest->z = pSrc->z;
        pDest->nx = pSrc->nx;
        pDest->ny = pSrc->ny;
        pDest->nz = pSrc->nz;
        pDest->tu1 = pSrc->tu0;
        pDest->tv1 = pSrc->tv0;
        pSrc += 1;
        pDest += 1;
    }
    pVB->Unlock();
    pVB->Release();
    pMeshVB->Unlock();

    //インデックスのコピー
    pMesh->GetIndexBuffer(&pSrcIndex);
    pSrcIndex->Lock(0,0,(BYTE**)&pISrc,0);
    pMeshIndex->Lock(0,0,(BYTE**)&pIDest,0);
    CopyMemory(pIDest,pISrc,nMeshFaces * 3 * sizeof(WORD));
    pSrcIndex->Unlock();
    pMeshIndex->Unlock();
    pSrcIndex->Release();

    // pD3DXMtrlBuffer から、質感やテクスチャーの情報を読み取る
    D3DXMATERIAL* d3dxMaterials = (D3DXMATERIAL*)pD3DXMtrlBuffer->GetBufferPointer();
    pMeshTextures = new LPDIRECT3DTEXTURE8[dwNumMaterials];
    pMeshMaterials = new D3DMATERIAL8[dwNumMaterials];

    for(i = 0; i < dwNumMaterials; i++){
        pMeshMaterials[i] = d3dxMaterials[i].MatD3D;
        pMeshMaterials[i].Ambient = pMeshMaterials[i].Diffuse;
        hr = D3DXCreateTextureFromFile( lpD3DDEV, 
                                        d3dxMaterials[i].pTextureFilename, 
                                        &pMeshTextures[i] );
        if(FAILED(hr)) pMeshTextures[i] = NULL;
    }
    RELEASE(pD3DXMtrlBuffer);
    
    RELEASE(pMesh);

    return S_OK;
}

先ず、D3DXLoadMeshFromX で、ファイルをロードします。
次に、Optimize で、パフォーマンスが上がるように作り変えてもらいます。
そして、GetAttributeTable で、属性(テクスチャ、レンダリング ステート、マテリアルの種類)がいくつあるかを数え、 同じ関数を格納する領域を作成してから呼び出して、内容を読み込みます。
次に、D3DFVF_VERTEX に、フォーマットを変換して、法線を計算します。
これで、基になるデータが出来ました。

次に、頂点フォーマットを変換します。
CreateVertexBuffer 及び、CreateIndexBuffer で、頂点バッファ、インデックス バッファを確保します。
その後に、pVB から xfile の頂点情報を読み込んで、新しく生成した頂点 pDest にひたすら書き込みます。 インデックス バッファは、変わらないので、単純にメモリコピーします。

あとは、以前行ったxfile の表示法と同じように、テクスチャーを読み込みます。

■表示

では、描画部分です。
mWorld, mView, mProj は、いつもどおりです。
そして、それらを SetVertexShaderConstant で、バーテックスシェーダーの定数レジスタに入れます。 定数レジスタに入れた部分は、黄色く色分けをしましたので、バーテックスシェーダープログラムと、比較しながら眺めてください。

テクスチャーの0番に pTexture (toon.bmp)、1番に pMeshTextures[i] (xfileに張られたテクスチャー)を設定します。 これで、oT0 にトゥーンのテクスチャー、oT1 が xfile のテクスチャーが出力されます。

あとは、ことごとく、SetTextureStageState で、テクスチャーの張り方の設定をしています。 それぞれの部分を見てください。
今回も、他のソースから引っ張ってきて、そのまま使っているのは、内緒です。
特筆する State は、テクスチャー1の D3DTSS_COLORARG2 です。 ここには、テクスチャー0で計算された結果が来ます。 この結果と、テクスチャー1の値を乗算することにより、テクスチャーが合成されます。

あとは、今回初登場の DrawIndexedPrimitive で描画します。SetIndices で設定するのが、新たに追加された部分です。

void Render(LPDIRECT3DDEVICE8 lpD3DDEV)
{
    if(NULL == pMeshVB) return;
    
    D3DXMATRIX mWorld, mView, mProj;

    D3DXMatrixRotationY( &mWorld, timeGetTime()/1000.0f );
    
    D3DXMatrixLookAtLH( &mView, &D3DXVECTOR3( 0.0f, 1.0f, 2.8f ), 
                                &D3DXVECTOR3( 0.0f, 0.0f, 0.0f ), 
                                &D3DXVECTOR3( 0.0f, 1.0f, 0.0f ) );
        
    D3DXMatrixPerspectiveFovLH(&mProj
        ,60.0f*PI/180.0f                        // 視野角
        ,(float)WIDTH/(float)HEIGHT                // アスペクト比
        ,0.01f                                    // 最近接距離
        ,100.0f                                    // 最遠方距離
        );
    
    D3DXMATRIX  m = mWorld * mView * mProj;
    D3DXMatrixTranspose( &m ,  &m);
    lpD3DDEV->SetVertexShaderConstant(0,&m, 4);           // c0~c3 : mWorld * mView * mProj
    D3DXMatrixTranspose( &m ,  &mWorld);
    lpD3DDEV->SetVertexShaderConstant(4, &m, 4);          // c4~c7 : mWorld
    D3DXMatrixInverse( &m,  NULL, &mWorld);
    lpD3DDEV->SetVertexShaderConstant(8, &m, 4);          // c8~c11 : mWorld の逆行列
                                                          // c12 : (0.0f, 0.5f, 1.0f, -1.0f)
    lpD3DDEV->SetVertexShaderConstant(12, D3DXVECTOR4(0.0f, 0.5f, 1.0f, -1.0f), 1);
    D3DXVECTOR4 lightDir(1.0f, 1.0f, 0.5f, 0.0f);
    D3DXVec4Normalize(&lightDir, &lightDir);
    lpD3DDEV->SetVertexShaderConstant(13, &lightDir, 1);  // c13 : ライトの方向
    
    lpD3DDEV->SetVertexShader(hVertexShader);
    lpD3DDEV->SetRenderState(D3DRS_WRAP0, D3DWRAP_U | D3DWRAP_V);             // U,V 方向共に、ループしたテクスチャーとして扱う
    lpD3DDEV->SetRenderState(D3DRS_WRAP1, D3DWRAP_U | D3DWRAP_V);             // 上のテクスチャー1に関する設定
    lpD3DDEV->SetTextureStageState(0, D3DTSS_COLOROP,      D3DTOP_MODULATE);  // ポリゴンの色 = ARG1 * ARG2
    lpD3DDEV->SetTextureStageState(0, D3DTSS_COLORARG1,    D3DTA_TEXTURE);    // ARG1 はテクスチャーの色
    lpD3DDEV->SetTextureStageState(0, D3DTSS_COLORARG2,    D3DTA_DIFFUSE);    // ARG2 はポリゴンの頂点色
    lpD3DDEV->SetTextureStageState(0, D3DTSS_MAGFILTER,    D3DTEXF_LINEAR);   // テクスチャを拡大する時の方法がバイリニア補間
    lpD3DDEV->SetTextureStageState(0, D3DTSS_MINFILTER,    D3DTEXF_LINEAR);   // テクスチャを縮小する時の方法がバイリニア補間
    lpD3DDEV->SetTextureStageState(0, D3DTSS_ADDRESSU,     D3DTADDRESS_CLAMP);// U,V 値が0及び1からはみ出した時は、その境界の値にする
    lpD3DDEV->SetTexture(0,pTexture);

    //メッシュの描画
    lpD3DDEV->SetStreamSource(0, pMeshVB, sizeof(D3D_CUSTOMVERTEX));
    lpD3DDEV->SetIndices(pMeshIndex,0);
    for(DWORD i=0;i < dwNumMaterials;i++){
        
        //色をセット
        D3DXVECTOR4 vl;
        vl.x = pMeshMaterials[i].Diffuse.r;
        vl.y = pMeshMaterials[i].Diffuse.g;
        vl.z = pMeshMaterials[i].Diffuse.b;
        lpD3DDEV->SetVertexShaderConstant(15,&vl,1);

        lpD3DDEV->SetTexture(1,pMeshTextures[i]);
        lpD3DDEV->SetTextureStageState(1, D3DTSS_COLOROP,   D3DTOP_MODULATE); // ポリゴンの色 = ARG1 * ARG2
        lpD3DDEV->SetTextureStageState(1, D3DTSS_COLORARG1, D3DTA_TEXTURE);   // ARG1 はテクスチャーの色
        lpD3DDEV->SetTextureStageState(1, D3DTSS_COLORARG2, D3DTA_CURRENT);   // ARG2 は前のブレンディング ステージ(oT0)の結果
        lpD3DDEV->SetTextureStageState(1, D3DTSS_MAGFILTER, D3DTEXF_LINEAR);  // テクスチャを拡大する時の方法がバイリニア補間
        lpD3DDEV->SetTextureStageState(1, D3DTSS_MINFILTER, D3DTEXF_LINEAR);  // テクスチャを縮小する時の方法がバイリニア補間

        lpD3DDEV->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 
                                        pSubsetTable[i].VertexStart,
                                        pSubsetTable[i].VertexCount,
                                        pSubsetTable[i].FaceStart * 3,
                                        pSubsetTable[i].FaceCount);
    }
    
    lpD3DDEV->SetTexture(0, NULL);
}

■後始末

さて、使ったメモリの開放です。
今回は、xfile 用に、DeleteMeshObject() を作りました。後は、バーテックスシェーダーと toon.bmp を開放します。

void CleanRender(LPDIRECT3DDEVICE8 lpD3DDEV)
{
    DeleteMeshObject();
    
    if ( hVertexShader != ~0 ){
        lpD3DDEV->DeleteVertexShader( hVertexShader );
        hVertexShader = ~0;
    }
    
    RELEASE(pTexture);
}

DeleteMeshObject() は、純粋に、使ったメモリを開放します。

void DeleteMeshObject(void)
{
    if(pMeshVB == NULL) return;

    for(DWORD i=0; i < dwNumMaterials; i++){
        RELEASE(pMeshTextures[i]);
    }
    delete[] pMeshTextures;
    delete[] pMeshMaterials;
    delete[] pSubsetTable;

    RELEASE(pMeshVB);
    RELEASE(pMeshIndex);
}

以上です。今回は長くて疲れましたね。

■最後に

ページのトップにある car.x では、トゥーンの効果が分かりにくいので、 DirectX のサンプルにある、tiger.x をレンダリングしてみました(サンプルプログラムの car.x を tiger.x に書き換えてください)。
これなら、それっぽく見えると思うのですが、いかがでしょうか?

これからも、このようなレンダリングの例をやっていこうと思います。





もどる

imagire@gmail.com