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: }
やっと次世代らしくなってきました。
今までに見れなかった画像を作りたいですね。
それにしても、おっせ~