Phong シェーディング


~ こ、これがNVIDIAの新型の威力なのかっ! ~






■はじめに

NV30 ならではの機能を使った効果を考えてみましょう。
NV30 の特徴の最大のものの1つは、ピクセルシェーダの大幅な機能拡張です。 命令数が増えたこともさることながら、一般的な命令が増えました。 たとえば、正規化の命令が入ったのもその1つです。
正規化ができるということは、法線の補間が綺麗にできるということです。 法線の綺麗な補間ができるということは、Phong シェーディングができるはずです。
では、やってみましょう。

実行には、「NVIDIA OpenGL Emulation Tool」を使って、NV30 Emulate をONにする必要があります(もちろん、NV30を持っていれば、そんなものはいりません)。 NVIDIA のサイトから拾ってきましょう。

今回のプログラムは次のものです。

まぁ、いつものように適当にファイルが入っています。
今回は、自前の行列演算ライブラリを使用します(そんなおおげさなものではないですが)。

vs.cg頂点シェーダー。
ps.cgフラグメントプログラム。
main.cpp一番大事な部分。
matrix.cpp簡易行列命令群。
matrix.h簡易行列命令群のインターフェイス。

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

■Phong シェーディング

Phong シェーディングは、ポリゴンの塗り方の方法です。
よく知られている方法として、べた塗りをする flat シェーディングや、 頂点間の色を線形補間する gouraud シェーディングが知られています。
Phong シェーディングは、法線ベクトルの大きさを保ったままで補間します。 わかりやすくいえば、法線を線形補間した後に、正規化して、法線ベクトルの大きさを1に保ってシェーディングします(必ずしもそうではない場合もありますが)。
正規化の部分をはしょって拡散光を計算したのが gouraud シェーディングともいえます。

Phong シェーディングの特徴の1つは、鏡面反射光の形が綺麗に丸になるということがあります。Gouraud シェーディングでは、形が直線的になるので、ポリゴンの形がわかってしまいます。
Phong シェーディングを実現する方法としては、正規化キューブマップが知られていますが、 全時代的な方法で、テクスチャー解像度に依存するので、できれば今回の方法を使ったほうが綺麗にシェーディングできます。
Phong シェーディングは、DirectX では、D3DSHADE_PHONG として、シェーディング モードが切られていますが、サポートされていないことがよく知られています。
NV30 世代になって、初めてコンシューマレベルで実現できるようになった方法です。

■Cgプログラム

今回は、頂点シェーダプログラムと、フラグメントプログラム(OpenGL におけるピクセルシェーダ)の2つのCg言語のプログラムを使います。

頂点シェーダプログラムを見ましょう。
今回は光源計算を行うので、入力データに法線を追加します。
また、法線データを頂点色として出力します。そのために、2つの頂点色を出力します。

vs.cg
0001: // ---------------------------------------------------------------------------
0002: // 頂点シェーダ入力データ
0003: // ---------------------------------------------------------------------------
0004: struct appdata
0005: {
0006:     float4 position : POSITION;
0007:     float3 normal   : NORMAL;
0008:     float3 color    : DIFFUSE;
0009: };
0010: 
0011: // ---------------------------------------------------------------------------
0012: // 頂点シェーダ出力データ
0013: // ---------------------------------------------------------------------------
0014: struct vfconn
0015: {
0016:     float4 HPOS : POSITION;
0017:     float4 COL0 : COLOR0;
0018:     float4 COL1 : COLOR1;
0019: };

頂点シェーダプログラムでは、座標変換とともに、入力として受け取った頂点色を出力します。
また、法線ベクトルをワールド座標系に変換した後に、0~1の範囲に範囲変更して、2つめの頂点色に出力します。

vs.cg
0021: // ---------------------------------------------------------------------------
0022: // Phong shading 頂点シェーダプログラム
0023: // ---------------------------------------------------------------------------
0024: vfconn main(appdata IN
0025:             , uniform float4x4 WorldViewProj
0026:             , uniform float4x4 World
0027:             , uniform float4   Light
0028:             )
0029: {
0030:     vfconn OUT;
0031: 
0032:     OUT.HPOS = mul(WorldViewProj, IN.position);
0033:     
0034:     // 頂点色
0035:     OUT.COL0.xyz = IN.color.xyz * (                         // モデルの色
0036:                       0.3f                                  // 環境光
0037:                     + 0.3f * dot(IN.normal.xyz, Light.xyz));// 拡散光
0038:     OUT.COL0.w = 1.0;
0039:     
0040:     // ワールド座標系の法線ベクトル
0041:     float4 n = IN.normal.xyzz;
0042:     n = mul(World, n);
0043: 
0044:     // 法線ベクトルを色で格納
0045:     OUT.COL1.xyz = 0.5f * n.xyz + 0.5f;
0046:     OUT.COL1.w = 0.5;
0047: 
0048:     return OUT;
0049: }

次に、フラグメントプログラムを見ていきましょう。
フラグメントプログラムで受け取るデータは、頂点シェーダの出力から座標値を取り除いた情報になリます。

ps.cg
0001: // ---------------------------------------------------------------------------
0002: // 頂点シェーダ出力データ
0003: // ---------------------------------------------------------------------------
0004: struct vpconn {
0005:     float4 color0 : COL0;   // 頂点色
0006:     float4 color1 : COL1;   // 法線ベクトル
0007: };

フラグメントプログラムでは、色情報として仕組まれた法線ベクトルを、 もとの-1から1の範囲に戻して、大きさが1になるように規格化します。
あとは、ハーフベクトルとの内積を計算して、鏡面反射光を求めます。
実際にプログラムしたところ、頂点色のα成分の情報がきちんと渡されていなかったのか、値がおかしかったので、最初に値を代入しました。
また、値を正の値に抑えるためのmaxの計算で、0との比較では、値が0にならなかったので、 小さな正の値(0.00000001f)を比較に使いました。

ps.cg
0009: // ---------------------------------------------------------------------------
0010: // Phong shading フラグメントプログラム
0011: // ---------------------------------------------------------------------------
0012: fragout main(vpconn I
0013:                 , uniform float4 Half
0014: ) {
0015:     fragout O;
0016: 
0017:     I.color1.w = 0.5;               // なぜかαに0.5が入ってない
0018:     float4 N = 2.0f*I.color1-1.0f;  // 法線ベクトルを-1~1に変える
0019:     N = normalize(N);               // 規格化
0020:     float  HN = max(dot(Half, N), 0.00000001f);
0021: 
0022:     O.col =   I.color0              // 環境色+拡散光
0023:             + 1.0f * pow(HN, 4);    // 反射光
0024: 
0025:     return O;
0026: } 

■呼び出し部分

では、残りのc言語部分を見ていきましょう。
今回は、頂点シェーダとフラグメントプログラムの両方を使うので、オブジェクトが2倍に増えています。
それぞれに、Contextやイテレータが必要になります。

main.cpp
0021: // Cg のオブジェクト
0022: static cgContext        *VertexContext          = NULL;
0023: static cgProgramIter    *VertexProgramIter      = NULL;
0024: static cgBindIter       *WorldViewProjBind      = NULL; // ワールド射影行列
0025: static cgBindIter       *WorldBind              = NULL; // ワールド行列
0026: static cgBindIter       *LightBind              = NULL; // ライトベクトル
0027: static cgBindIter       *ColorBind              = NULL; // 頂点色
0028: 
0029: static cgContext        *FragmentContext        = NULL;
0030: static cgProgramIter    *FragmentProgramIter    = NULL;
0031: static cgBindIter       *HalfBind               = NULL; // ハーフべクトルの方向

初期化の段階では、それぞれのプログラムをcgAddProgramFromFileで読み込んでから、 cgProgramByName で、シェーダプログラムの開始関数を調べます。
実際にCgプログラムを使うためには、cgGLLoadProgramでグラフィックボードに転送します。

main.cpp
0256: // ---------------------------------------------------------------------------
0257: // Cgの初期化
0258: // ---------------------------------------------------------------------------
0259: static void InitializeCg()
0260: {
0261:     cgError Ret;
0262:     
0263:     // Cg の作成
0264:     VertexContext = cgCreateContext();
0265:     assert(VertexContext != NULL);
0266: 
0267:     // Cg の作成
0268:     Ret = cgAddProgramFromFile(VertexContext, "vs.cg", cgVertexProfile, NULL);
0269:     fprintf(stderr, "LAST LISTING----%s----\n", cgGetLastListing(VertexContext));
0270:     assert(Ret == cgNoError);
0271: 
0272:     // プログラム名でプログラムを結びつける
0273:     VertexProgramIter = cgProgramByName(VertexContext, "main");
0274:     assert(VertexProgramIter != NULL);
0275: 
0276:     // ソースの表示
0277:     fprintf(stderr, "---- プログラム はじめ ----\n"
0278:                     "%s"
0279:                     "---- プログラム 終わり ----\n",
0280:     cgGetProgramObjectCode(VertexProgramIter));
0281: 
0282:     if(VertexProgramIter != NULL) {
0283:         GLuint ProgId = 1;
0284: 
0285:         Ret = cgGLLoadProgram(VertexProgramIter, ProgId);
0286:         assert(Ret == cgNoError);
0287:         // 変換行列を結びつける
0288:         WorldViewProjBind = cgGetBindByName(VertexProgramIter, "WorldViewProj");
0289:         assert(WorldViewProjBind != NULL);
0290:         // ワールド行列を結びつける
0291:         WorldBind = cgGetBindByName(VertexProgramIter, "World");
0292:         assert(WorldBind != NULL);
0293:         // ライトの方向を結びつける
0294:         LightBind = cgGetBindByName(VertexProgramIter, "Light");
0295:         assert(LightBind != NULL);
0296:         // 頂点色
0297:         ColorBind = cgGetBindByName(VertexProgramIter, "IN.color");
0298:         assert(ColorBind != NULL);
0299:     }
0300: 
0301:  
0302:     // fragment program setup
0303:     FragmentContext = cgCreateContext();
0304:     assert(FragmentContext != NULL);
0305: 
0306:     Ret = cgAddProgramFromFile(FragmentContext, "ps.cg", cgFragmentProfile, NULL);
0307:     assert(Ret == cgNoError);
0308: 
0309:     // プログラム名でプログラムを結びつける
0310:     FragmentProgramIter = cgProgramByName(FragmentContext, "main");
0311:     assert(FragmentProgramIter != NULL);
0312: 
0313:     // ソースの表示
0314:     fprintf(stderr, "---- プログラム はじめ ----\n"
0315:                     "%s"
0316:                     "---- プログラム 終わり ----\n",
0317:     cgGetProgramObjectCode(FragmentProgramIter));
0318: 
0319:     if(FragmentProgramIter != NULL)
0320:     {
0321:         GLuint ProgId = 2;
0322: 
0323:         Ret = cgGLLoadProgram(FragmentProgramIter, ProgId);
0324:         assert(Ret == cgNoError);
0325: 
0326:         // ライト方向
0327:         HalfBind = cgGetBindByName(FragmentProgramIter, "Half");
0328:         assert(HalfBind != NULL);
0329:     }
0330:     
0331: }

後片付けは、それぞれにFree関数を呼び出します。

main.cpp
0332: // ---------------------------------------------------------------------------
0333: // Cg の後片付け
0334: // ---------------------------------------------------------------------------
0335: static void CleanupCg()
0336: {
0337:     // フラグメント
0338:     if(HalfBind)cgFreeBindIter( HalfBind );
0339:     cgFreeProgramIter( FragmentProgramIter );
0340:     cgFreeContext( FragmentContext );
0341: 
0342:     // 頂点シェーダ
0343:     if(LightBind        )cgFreeBindIter( LightBind );
0344:     if(WorldBind        )cgFreeBindIter( WorldBind );
0345:     if(WorldViewProjBind)cgFreeBindIter( WorldViewProjBind );
0346:     if(ColorBind        )cgFreeBindIter( ColorBind );
0347:     cgFreeProgramIter( VertexProgramIter );
0348:     cgFreeContext( VertexContext );
0349:     cgCleanup( );
0350: }

さて、描画部分です。
最初は、画面クリアしたり、ビュー行列を作成します。

main.cpp
0105: // ---------------------------------------------------------------------------
0106: // 画面描画
0107: // ---------------------------------------------------------------------------
0108: void display(void)
0109: {
0110:     ifMatrix m, m0;
0111:     cgError Ret;
0112:     
0113:     // -----------------------------------------------------------------------
0114:     // 画面のクリア
0115:     // -----------------------------------------------------------------------
0116:     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
0117: 
0118:     // -----------------------------------------------------------------------
0119:     // ビュー行列の設定
0120:     // -----------------------------------------------------------------------
0121:     glMatrixMode(GL_MODELVIEW);
0122:     glLoadIdentity();
0123:     gluLookAt(  0.0, 0.0, 5.0,      // 視点
0124:                 0.0, 0.0, 0.0,      // 注目点
0125:                 0.0, 1.0, 0.0);     // 上方向
0126: 
0127:     glPushMatrix();                 // ビュー行列が他のポリゴンでも使えるよう退避

その後に、シェーダの設定をしましょう。

main.cpp
0129:     // -----------------------------------------------------------------------
0130:     // Cg による描画へ切り替え
0131:     // -----------------------------------------------------------------------
0132:     Ret = cgGLBindProgram(VertexProgramIter);
0133:     assert(Ret == cgNoError);
0134:     cgGLEnableProgramType(cgVertexProfile);
0135: 
0136:     Ret = cgGLBindProgram(FragmentProgramIter);
0137:     assert(Ret == cgNoError);
0138:     cgGLEnableProgramType(cgFragmentProfile);

後は、シェーダへの引数を受け渡します。 cgGLBindUniform***で、値をシェーダプログラムに送ります。

main.cpp
0140:     // -----------------------------------------------------------------------
0141:     // 行列を設定する
0142:     // -----------------------------------------------------------------------
0143:     ifMatrixIdentity( &m );
0144:     ifMatrixRotationZ(&m0, -IF_PI*20/180);
0145:     ifMatrixMultiply( &m, (const ifMatrix *)&m, (const ifMatrix *)&m0 );
0146:     ifMatrixRotationX(&m0,  IF_PI*60/180);
0147:     ifMatrixMultiply( &m, (const ifMatrix *)&m, (const ifMatrix *)&m0 );
0148:     ifMatrixRotationY(&m0,  IF_PI* r/180);
0149:     ifMatrixMultiply( &m, (const ifMatrix *)&m, (const ifMatrix *)&m0 );
0150:     
0151:     glMultMatrixf((const float *)&m);
0152: 
0153:     // ワールド行列を設定する
0154:     cgGLBindUniformMatrixcf(VertexProgramIter, WorldBind, (const float *)&m[0][0]);
0155:     // ワールド - 射影行列
0156:     if(WorldViewProjBind != NULL) {
0157:         cgGLBindUniformStateMatrix(VertexProgramIter, 
0158:                                 WorldViewProjBind,
0159:                                 cgGLModelViewProjectionMatrix,
0160:                                 cgGLMatrixIdentity);
0161:     }
0162: 
0163:     // -----------------------------------------------------------------------
0164:     // ライトの設定
0165:     // -----------------------------------------------------------------------
0166:     ifVector4 dir = {0.3f, 0.3f, 0.1f, 0.0f};
0167:     ifVec4Normalize(&dir, &dir);
0168:     
0169:     // 擬似ハーフベクトルを設定する
0170:     ifVector4 eye = {0,0,1,0};// ここが定数が近似
0171:     ifVector4 half;
0172:     ifVec4Normalize(&half, ifVec4Add(&half, &dir, &eye));
0173:     cgGLBindUniform4f(FragmentProgramIter, HalfBind
0174:                 ,half[0], half[1], half[2], half[3]);
0175:     
0176:     // (ローカル座標の)ライトベクトルを設定する
0177:     ifMatrixTranspose( &m, (const ifMatrix *)&m );
0178:     ifVec4Transform(&dir, &dir, (const ifMatrix *)&m);
0179:     cgGLBindUniform4f(VertexProgramIter, LightBind
0180:                 ,dir[0], dir[1], dir[2], dir[3]);

今回、行列計算に独自の関数を作って、ワールド行列を作った後に glMultMatrixf で、 ワールド行列として設定しました。
これは、ワールド行列そのものを頂点シェーダプログラムに渡す必要があるからです。 また、光源ベクトルの計算をローカル座標で行うために、 ワールド行列の逆変換を光源ベクトルに作用してから、シェーダプログラムにためにも必要です。
ハーフベクトルは、本来は、視点方向へのベクトルとライトベクトルの中間のベクトルですが、頂点シェーダから渡すときに、色の出力レジスタが足りなくなって(テクスチャ座標を使うのが面倒くさかったので)、近似計算として一様な視線ベクトルを使いました。

独自行列は、

matrix.h
0015: typedef float ifVector4[4];
0016: 
0017: typedef float ifMatrix[4][4];

の形で行列、ベクトルを定義して、それらの関数を作りました。

後は、描画部分ですが、これは描画関数を呼び出します。

main.cpp
0182:     // -----------------------------------------------------------------------
0183:     // 箱の描画
0184:     // -----------------------------------------------------------------------
0185:     
0186:     DrawCube();
0187: 

描画関数の中身は、次のように、法線や頂点及び頂点色を次々に送り込みます。
頂点色は、頂点座標を0から1の間に変換した色を使ってグラデーションを作りました。この色は箱の色なので、適当に設定してください。

main.cpp
0059: // ---------------------------------------------------------------------------
0060: // 箱の描画
0061: // ---------------------------------------------------------------------------
0062: static void DrawCube(void)
0063: {
0064:     int i;
0065: 
0066:     // 箱の描画
0067:     for(i = 0; i < 6; i++) {
0068:         glBegin(GL_QUADS);  // 四角形の描画
0069: 
0070:         cgGLBindVarying3f(VertexProgramIter, ColorBind
0071:             , 0.5f*CubeVertices[CubeFaces[i][0]][0]+0.5f
0072:             , 0.5f*CubeVertices[CubeFaces[i][0]][1]+0.5f
0073:             , 0.5f*CubeVertices[CubeFaces[i][0]][2]+0.5f
0074:             );  // 頂点色
0075:         glNormal3fv(&CubeNormals [CubeFaces[i][0]][0]);             // 法線
0076:         glVertex3fv(&CubeVertices[CubeFaces[i][0]][0]);             // 座標
0077: 
0078:         cgGLBindVarying3f(VertexProgramIter, ColorBind
0079:             , 0.5f*CubeVertices[CubeFaces[i][1]][0]+0.5f
0080:             , 0.5f*CubeVertices[CubeFaces[i][1]][1]+0.5f
0081:             , 0.5f*CubeVertices[CubeFaces[i][1]][2]+0.5f
0082:             );  // 頂点色
0083:         glNormal3fv(&CubeNormals [CubeFaces[i][1]][0]);
0084:         glVertex3fv(&CubeVertices[CubeFaces[i][1]][0]);
0085: 
0086:         cgGLBindVarying3f(VertexProgramIter, ColorBind
0087:             , 0.5f*CubeVertices[CubeFaces[i][2]][0]+0.5f
0088:             , 0.5f*CubeVertices[CubeFaces[i][2]][1]+0.5f
0089:             , 0.5f*CubeVertices[CubeFaces[i][2]][2]+0.5f
0090:             );  // 頂点色
0091:         glNormal3fv(&CubeNormals [CubeFaces[i][2]][0]);
0092:         glVertex3fv(&CubeVertices[CubeFaces[i][2]][0]);
0093: 
0094:         cgGLBindVarying3f(VertexProgramIter, ColorBind
0095:             , 0.5f*CubeVertices[CubeFaces[i][3]][0]+0.5f
0096:             , 0.5f*CubeVertices[CubeFaces[i][3]][1]+0.5f
0097:             , 0.5f*CubeVertices[CubeFaces[i][3]][2]+0.5f
0098:             );  // 頂点色
0099:         glNormal3fv(&CubeNormals [CubeFaces[i][3]][0]);
0100:         glVertex3fv(&CubeVertices[CubeFaces[i][3]][0]);
0101:     
0102:         glEnd();    // 四角形終わり
0103:     }
0104: }

描画が終わったら、Cgによるシェーダをやめて、画面を切り替えます。

main.cpp
0188:     // -----------------------------------------------------------------------
0189:     // 描画の終了
0190:     // -----------------------------------------------------------------------
0191:     cgGLDisableProgramType(cgFragmentProfile);
0192:     cgGLDisableProgramType(cgVertexProfile);
0193: 
0194:     glPopMatrix();                  // ビュー行列に現在の行列を戻す
0195:     glutSwapBuffers();              // 画面の更新の終了
0196: }

■最後に

やっと次世代らしくなってきました。
今までに見れなかった画像を作りたいですね。

それにしても、おっせ~





もどる

imagire@gmail.com