頂点ブレンド


~ぐねぐね~




■はじめに

まぁ、やっていないネタはいくつかあるのですが、 頂点を動かしていろいろやる効果はまだやっていなかったので、やりましょう。

いいかんじに矢印が別の方向を示しています(地球に意味はありません)。
今回のソースは、次のものです(DirectX8.1 & Cg言語 用です)。

今回は、いろいろといじくれます。

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

vs.cg頂点シェーダー。「Cg言語」
ps.cgピクセルシェーダー。「Cg言語」
draw.cppメインの描画部分。
draw.h描画の各関数の定義。ひそかにカメラのソースが入ってる。
bgvs.cg背景オブジェクト用頂点シェーダー。前回と同じ。
bgps.cg背景オブジェクト用ピクセルシェーダー。前回と同じ。
bg.cpp背景オブジェクトのメッシュを作ったり描画したり。
main.h基本的な定数など。今回も出番無し。
main.cpp描画に関係しないシステム的な部分。変更が無いので、出番無し。
load.cppロード。頂点ブレンドのブレンドファクターの計算もする。
load.hロードのインターフェイス。
arrow.bmp (矢印で使っている)
earth.bmp (地球テクスチャー)
tile.bmp (床デカール)
sky.bmp (空デカール)

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

■やっていること

頂点ブレンディングの方法ですが、2つの状態を用意して、その間をつなぎます。
2つの状態とは、具体的には姿勢を制御する行列になります。
2つの行列を用意して、頂点ごとに重み付けした割合で行列を線形合成します。
例えば、下の例なら、上向きの矢印(単位行列)と左向きの矢印(z軸回転した行列)を用意し、 根元の行列は上向きに、先頭の行列は左向きに一致するように係数を決めます。
具体的には、根元は w0=1, w1=0 で先端は w0=0, w1=1 にして、途中は線形に係数を設定します。

■頂点シェーダー(vs.cg)

さて、それでは、プログラムを見ていきましょう。
要の頂点シェーダープログラムを見たいと思います。
入力頂点「appdata」に合成用のパラメータ「Weight」を追加しました(ライティング用のNormalも追加しましたが)。
実際には、この変数のx成分とy成分を合成の係数とします。

0005: // ----------------------------------------------------------------------------
0006: // 入力用構造体
0007: // ----------------------------------------------------------------------------
0008: struct appdata : application2vertex {
0009:     float4 Position;
0010:     float4 Weight;
0011:     float4 Normal;
0012:     float2 Texcoord0;
0013: };
0014: 
0015: // ----------------------------------------------------------------------------
0016: // 出力用構造体
0017: // ----------------------------------------------------------------------------
0018: #pragma bind vfconn.Position  = HPOS;
0019: #pragma bind vfconn.Diffuse   = COL0;
0020: #pragma bind vfconn.Texcoord0 = TEX0;
0021: 
0022: struct vfconn : vertex2fragment {
0023:     float4 Position;
0024:     float4 Diffuse;
0025:     float2 Texcoord0;
0026: };

入力頂点「appdata」に合成用のパラメータ「Weight」を追加しました(ライティング用のNormalも追加しましたが)。
実際には、この変数のx成分とy成分を合成の係数とします。
出力にも頂点色の「Diffuse」を追加しました。
また、今回は、頂点シェーダーとピクセルシェーダーの掛け橋になる命令である「#pragma bind」を使いました。 この命令は、実際にどのようなレジスタに構造体の内容がコピーされるかをコンパイラに知らせるためのものです。

プログラムの関数部分ですが、座標計算が一番のポイントです。黄色の部分になります。
それぞれの行列で計算した座標を合成係数を使って合成します。
合成した行列に「ワールド」―「スクリーン」変換を施して、最終的な結果にします。

0027: // ----------------------------------------------------------------------------
0028: // 頂点シェーダープログラム
0029: // ----------------------------------------------------------------------------
0030: vfconn main(appdata IN
0031:         , uniform float4x4 worldviewproj_matrix
0032:         , uniform float3x4 m0
0033:         , uniform float3x4 m1
0034:         , uniform float4 light
0035:         , uniform float4 diffuse
0036: ) {
0037:     vfconn OUT;
0038:     float4 pos;
0039: 
0040:     /////////////////////////////////////////////////////////////////////
0041:     // 座標
0042:     float3 pos0 = mul(m0, IN.Position);     // m0 で変換した座標
0043:     float3 pos1 = mul(m1, IN.Position);     // m1 で変換した座標
0044:     
0045:     // 座標を合成する
0046:     pos.xyz = IN.Weight.x * pos0        // out = (w0*m0+w1*m1)*in
0047:             + IN.Weight.y * pos1;
0048:     pos.w = 1.0f;
0049: 
0050:     // 行列をかけて、頂点をスクリーン座標に変換する
0051:     OUT.Position = mul(worldviewproj_matrix, pos);
0052: 
0053:     /////////////////////////////////////////////////////////////////////
0054:     // 頂点色座標
0055:     float3 n0 = mul(m0, IN.Normal);     // m0 で変換した座標
0056:     float3 n1 = mul(m1, IN.Normal);     // m1 で変換した座標
0057:     float3 n = IN.Weight.x * n0 + IN.Weight.y * n1;
0058:     
0059:     float4 LN = max(dot(n, light.xyz), 0).xxxx + light.w;
0060: 
0061:     OUT.Diffuse = diffuse * LN;         // 頂点の色をそのままコピー
0062: 
0063:     /////////////////////////////////////////////////////////////////////
0064:     // テクスチャー座標
0065:     OUT.Texcoord0 = IN.Texcoord0;   // そのままコピーする
0066: 
0067:     return OUT;
0068: }

法線も同様の計算をして、ワールド座標での法線を求めてから、平行光源の計算を計算をして(元の頂点色の影響を考慮して)最終的な頂点色にします。
計算を見ても分かるとおり、法線の計算を頂点と同じ計算をしているので、スケールの入った光源計算には対応していないことになります。
ほんとに考慮するなら、逆転地行列を求めなければいけないですね(どうやるんだ?)。

シェーダープログラムのコンパイル結果は、

vs.1.1
//const c 12 = 1 0 0 0
dp4 r0.x, c4, v0
dp4 r0.y, c5, v0
dp4 r0.z, c6, v0
dp4 r1.x, c7, v0
dp4 r1.y, c8, v0
dp4 r1.z, c9, v0
mul r1, v1.y, r1.xyz
mad r0.xyz, v1.x, r0.xyz, r1.xyz
mov r0.w, c12.x
dp4 r1.x, c0, r0
dp4 r1.y, c1, r0
dp4 r1.z, c2, r0
dp4 r1.w, c3, r0
mov oPos, r1
dp4 r0.x, c4, v2
dp4 r0.y, c5, v2
dp4 r0.z, c6, v2
dp4 r1.x, c7, v2
dp4 r1.y, c8, v2
dp4 r1.z, c9, v2
mul r1, v1.y, r1.xyz
mad r0, v1.x, r0.xyz, r1.xyz
dp3 r0, r0.xyz, c10.xyz
max r0, r0.x, c12.y
add r0, r0.x, c10.w
mul r0, c11, r0
mov oD0, r0
mov oT0.xy, v3

でした。確かに説明したことを実行していますが、コメントつけないと何がなんだかわかりませんな。

■ピクセルシェーダー

ピクセルシェーダープログラムです。
やはり、「bind」を使って、レジスタと変数を結び付けます。

0001: // ----------------------------------------------------------------------------
0002: // 頂点シェーダーからの入力構造体
0003: // ----------------------------------------------------------------------------
0004: #pragma bind v2f_simple.position  = HPOS;
0005: #pragma bind v2f_simple.diffuse   = COL0;
0006: #pragma bind v2f_simple.texcoord0 = TEX0;
0007: 
0008: struct v2f_simple : vertex2fragment  {
0009:     float4 position;
0010:     float4 diffuse;
0011:     float4 texcoord0;
0012: };
0013: // ----------------------------------------------------------------------------
0014: fragout main(v2f_simple I
0015:                 , uniform sampler2D tex0
0016: ) {
0017:   fragout O;   
0018: 
0019:     // 出力 = 頂点色 x テクスチャー
0020:     O.col = I.diffuse * tex2D(tex0);
0021: 
0022:   return O;
0023: } 

内容は頂点色とテクスチャーの色を反映させている簡単なものです。

シェーダープログラムのコンパイル結果は、

ps.1.1

tex t0
mul r0, v0, t0

になりました。

■C言語

それでは、残った部分で重要な部分に関して説明しましょう。
重みデータの作成ですが、メッシュのデータを加工するときに同時に設定します。
ロードしたデータは、

load.cpp
0018: typedef struct {
0019:     float x,y,z;
0020:     float nx,ny,nz;
0021:     float tu0,tv0;
0022: }D3DVERTEX;

です、一方、シェーダーに送るデータは、

load.h
0018: typedef struct {
0019:     D3DXVECTOR4  position;
0020:     D3DXVECTOR4  weight;
0021:     D3DXVECTOR4  normal;
0022:     D3DXVECTOR2  texcoord0;
0023: }D3D_CUSTOMVERTEX;

の、形式になります。
ロードした後にシェーダーに送るデータを作るときに、次のようにデータを作成します。

load.cpp
0092:     for(i=0;i<nMeshVertices;i++){
0093:         pDest->position[0] = pSrc->x;
0094:         pDest->position[1] = pSrc->y;
0095:         pDest->position[2] = pSrc->z;
0096:         pDest->position[3] = 1.0f;
0097:         pDest->normal[0] = pSrc->nx;
0098:         pDest->normal[1] = pSrc->ny;
0099:         pDest->normal[2] = pSrc->nz;
0100:         pDest->normal[3] = 0.0f;
0101:         pDest->texcoord0[0] = pSrc->tu0;
0102:         pDest->texcoord0[1] = pSrc->tv0;
0103:         
0104:         pDest->weight[1] = pSrc->y;
0105:         pDest->weight[0] = 1.0f-pDest->weight[1];
0106: 
0107:         pSrc++;
0108:         pDest++;
0109:     }

肝心の「重み」部分ですが、元のモデルをy軸方向に大きさ1で作りました。
そこで、高さをそのまま重みにします。
また、2つめの重みは「足した結果は1」になるように決定しました。
「足した結果は1」2つの行列を「線形に」合成するための条件です。
あえて、この条件を取り外して、面白い演出を狙うのも手だと思います。

後は、実際にブレンドする行列の求め方です。
根元の行列は、角度と位置をグローバル変数としてもって、設定しています。
先端の行列は、根元より0.4(半分より少し下の位置)だけ上の位置に指し示す中心となる位置を決めておき、そこから方向を求めます。
あとは、D3DXMatrixLookAtLH が内部で行っているのと同じように規定行列 e1,e2,e3 を求め、 先端の位置を中心に回転させます。
先端の位置を中心に回転するのは、平行移動で、先端の位置を中心に平行移動してから回転(基底行列の変換)をします。
計算はごちゃごちゃしていますが、いい感じになるように適当に動かしていると思ってください。

draw.cpp
0211: VOID Render(LPDIRECT3DDEVICE8 lpD3DDev)
0212: {
0213:     D3DXMATRIX mWorld, mProj, m, m0, m1;
0214:     
0215:     // 射影行列
0216:     D3DXMatrixPerspectiveFovLH(&mProj
0217:         ,60.0f*D3DX_PI/180.0f                   // 視野角
0218:         ,(float)WIDTH/(float)HEIGHT             // アスペクト比
0219:         ,0.01f,100.0f                           // 最近接距離,最遠方距離
0220:         );
0221:     D3DXMATRIX mWS = view.Get() * mProj;

0230:     FrameMove(lpD3DDev, mWS); // 動かす
0231:     
0232:     lpD3DDev->SetTextureStageState(0,D3DTSS_COLOROP,    D3DTOP_MODULATE);
0233:     lpD3DDev->SetTextureStageState(0,D3DTSS_COLORARG1,  D3DTA_TEXTURE);
0234:     lpD3DDev->SetTextureStageState(0,D3DTSS_COLORARG2,  D3DTA_DIFFUSE);
0235:     lpD3DDev->SetTextureStageState(1,D3DTSS_COLOROP,    D3DTOP_DISABLE);
0236: 
0237:     // プログラマぶるシェーダーを有効にする
0238:     pVs->SetShaderActive();
0239:     pPs->SetShaderActive();
0240:     
0241:     D3DXMatrixTranslation(&m0 , position.x, position.y, position.z);
0242:     D3DXMatrixRotationX(&m, angle.x );  m0 = m * m0;
0243:     D3DXMatrixRotationY(&m, angle.y );  m0 = m * m0;
0244:     D3DXMatrixRotationZ(&m, angle.z );  m0 = m * m0;
0245: 
0246:     D3DXMATRIX mT, mTi, mR;
0247:     D3DXVECTOR3 up = D3DXVECTOR3(0.0f,1.0f,0.0f);
0248:     D3DXVECTOR3 v = *(D3DXVECTOR3*)&position1-D3DXVECTOR3(0.0f,0.4f,0.0f);
0249:     D3DXVECTOR3 e1, e2, e3;
0250:     D3DXVec3Cross(&e1, &v,  &up);D3DXVec3Normalize(&e1,&e1);
0251:     D3DXVec3Cross(&e2, &e1, &v );D3DXVec3Normalize(&e2,&e2);
0252:     D3DXVec3Cross(&e3, &e1, &e2);D3DXVec3Normalize(&e3,&e3);
0253:     D3DXMatrixIdentity(&mR);
0254:     mR._11 = e1.x;  mR._12 = e1.y;  mR._13 = e1.z;
0255:     mR._21 =-e3.x;  mR._22 =-e3.y;  mR._23 =-e3.z;
0256:     mR._31 = e2.x;  mR._32 = e2.y;  mR._33 = e2.z;
0257:     D3DXMatrixTranslation(&mT , 0.0f, 1.0f, 0.0f);
0258:     D3DXMatrixTranslation(&mTi, 0.0f,-1.0f, 0.0f);
0259:     D3DXMatrixTranslation(&m1, position1.x, position1.y, position1.z);
0260:     m1 = mTi*mR*mT*m1;
0261: 
0262:     // 行列を設定
0263:     pVs->SetShaderConstant( vertex_mat_iter,D3DXMatrixTranspose( &m, &mWS )  );
0264:     pVs->SetShaderConstant( mat0_iter,      D3DXMatrixTranspose( &m, &m0 )  );
0265:     pVs->SetShaderConstant( mat1_iter,      D3DXMatrixTranspose( &m, &m1 )  );
0266: 
0267:     lpD3DDev->SetStreamSource(0, mesh.pVB, sizeof(D3D_CUSTOMVERTEX));
0268:     lpD3DDev->SetIndices(mesh.pIndex,0);
0269:     
0270:     for(DWORD i=0;i0271:         //色をセット
0272:         D3DXVECTOR4 vl(
0273:             mesh.pMaterials[i].Diffuse.r,
0274:             mesh.pMaterials[i].Diffuse.g,
0275:             mesh.pMaterials[i].Diffuse.b,
0276:             1.0f);
0277:         pVs->SetShaderConstant( diffuse_iter, &vl  );
0278:         pPs->SetTexture(tex0_iter, mesh.pTextures[i]);
0279: 
0280:         lpD3DDev->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 
0281:                                         mesh.pSubsetTable[i].VertexStart,
0282:                                         mesh.pSubsetTable[i].VertexCount,
0283:                                         mesh.pSubsetTable[i].FaceStart * 3,
0284:                                         mesh.pSubsetTable[i].FaceCount);
0285:     }
0287: }

■問題点などなど

実は、今回の矢印のモデルは、ポリゴン数が非常に多いです(3700トライアングルぐらい)。
これだけポリゴン数があると、まげても綺麗に見えます。
こんなプログラムを書くと、曲面レンダリングをしてるんじゃないかと誤解されるので、 ポリゴン数を削ったモデルで表示すると、次のようになります。


さすがに、ポリゴンがひん曲がっているようにしか見えません。
今回のプログラムは、モデルのポリゴンの形を気をつけないといけません。

■最後に

まぁ、聡明な方はお分かりだと思うのですが、とあるもの(DirectXの質問でタブーになってるやつですな)の前振りです。
今回のものでも、モーフィング等に使えたりするのではないでしょうか。
さて、次回はすんなりいくかどうか・・・




もどる

imagire@gmail.com