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 シェーディングは、ポリゴンの塗り方の方法です。
よく知られている方法として、べた塗りをする flat シェーディングや、
頂点間の色を線形補間する gouraud シェーディングが知られています。
Phong シェーディングは、法線ベクトルの大きさを保ったままで補間します。
わかりやすくいえば、法線を線形補間した後に、正規化して、法線ベクトルの大きさを1に保ってシェーディングします(必ずしもそうではない場合もありますが)。
正規化の部分をはしょって拡散光を計算したのが gouraud シェーディングともいえます。
Phong シェーディングの特徴の1つは、鏡面反射光の形が綺麗に丸になるということがあります。Gouraud シェーディングでは、形が直線的になるので、ポリゴンの形がわかってしまいます。
Phong シェーディングを実現する方法としては、正規化キューブマップが知られていますが、
全時代的な方法で、テクスチャー解像度に依存するので、できれば今回の方法を使ったほうが綺麗にシェーディングできます。
Phong シェーディングは、DirectX では、D3DSHADE_PHONG として、シェーディング モードが切られていますが、サポートされていないことがよく知られています。
NV30 世代になって、初めてコンシューマレベルで実現できるようになった方法です。
今回は、頂点シェーダプログラムと、フラグメントプログラム(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: }
やっと次世代らしくなってきました。
今までに見れなかった画像を作りたいですね。
それにしても、おっせ~