前回はダブルバッファリングとFPSについて説明しました。
そろそろ画面に図形の1つでも表示したいところですが、OpenGLのプログラムだけでは画面に絵を出すことはできません。絵を出すにはシェーダを書く必要があります。そこで、今回はOpenGLで使うシェーダ言語、GLSLについて説明します。
今回の記事の内容は次のとおりです。
シェーダとは
シェーダ(Shader)とは名前からも分かるとおり影をつける計算をするためのものです。下の記事でも紹介しているとおり影を適切につけることで平面のものを立体に見せることが出来ます。
OpenGLとGLSLを使ってプログラミングをする流れは次のようになります。
- OpenGLでは「どのモデルをどこに描画するか」を決めます
- シェーダではそのモデルを正しい位置に、指定された色で描画します
- バーテックスシェーダではモデルを正しい位置に表示する計算を行います
- フラグメントシェーダではモデルのシェーディングを行います
では、バーテックスシェーダとフラグメントシェーダが具体的にどのような働きをするのかを簡単にみていきましょう。
バーテックスシェーダとは
上にも書いた通り、バーテックスシェーダではOpenGLからモデルのもつ各頂点座標を受け取り、その頂点を空間内のどこに頂点を配置するかを計算します。
バーテックスシェーダは頂点単位で処理を行うプログラムです。頂点が6個ある立方体なら、バーテックスシェーダの処理が6回実行されます。もちろん5万個の頂点があるモデルなら5万回バーテックスシェーダが実行されます。
今回作るバーテックスシェーダのプログラムは次のとおりです。このシェーダではOpenGLから受け取った頂点座標をそのまま出力しています。先ほど作成したsimple.vertを開いて次のプログラムを入力して下さい。
attribute vec3 position; void main(void){ gl_Position = vec4(position, 1.0); }
GLSL特有の書き方もあるので簡単に説明します。一行目のattributeキーワードはOpenGLからデータを受け取る際に使います。今回作成したバーテックスシェーダでは、OpenGLから頂点座標を受け取り、受け取った座標に何も変更を加えずにそのまま出力しています。
gl_PositionはGLSLで定義された特別な変数で、gl_Positionに代入した値がバーテックスシェーダの出力になります。
フラグメントシェーダとは
ピクセルシェーダではバーテックスシェーダから出力された頂点座標の位置を元にモデルにシェーディング(影つけ)を行います。
バーテックスシェーダは頂点単位の処理でしたが、フラグメントシェーダはピクセル単位で処理が進みます。このように、ピクセルシェーダは画面上のピクセル回数分実行されるため、比較的重たい処理になる傾向があります。
フラグメントシェーダのプログラムは次のようになります。いま作成したsimple.fragを開いて、次のプログラムを入力して下さい。
void main(void){ gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); }
このフラグメントシェーダではモデル上にあるピクセルを全て白色に塗りつぶしています。gl_FlagColorもGLSLで定義された特別な変数で、gl_FragColorで定義した色が最終的なピクセルの色になります
シェーダをOpenGLのプログラムに登録する
では、実際に上で説明したバーテックスシェーダとフラグメントシェーダを使うプログラムを見ていきます。ここで書いているLoadShader関数は最初から理解する必要はありません。この関数ではバーテックスシェーダとフラグメントシェーダのコンパイルとリンクをしていることだけ覚えておきましょう。
#include <GLFW/glfw3.h> #include <iostream> #include <fstream> const int g_width = 640; const int g_height = 480; GLuint crateShader() { //バーテックスシェーダのコンパイル GLuint vShaderId = glCreateShader(GL_VERTEX_SHADER); string vertexShader = R"#( attribute vec3 position; void main(void){ gl_Position = vec4(position, 1.0); } )#"; const char* vs = vertexShader.c_str(); glShaderSource(vShaderId, 1, &vs, NULL); glCompileShader(vShaderId); //フラグメントシェーダのコンパイル GLuint fShaderId = glCreateShader(GL_FRAGMENT_SHADER); string fragmentShader = R"#( void main(void){ gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); } )#"; const char* fs = fragmentShader.c_str(); glShaderSource(fShaderId, 1, &fs, NULL); glCompileShader(fShaderId); //プログラムオブジェクトの作成 GLuint programId = glCreateProgram(); glAttachShader(programId,vShaderId); glAttachShader(programId,fShaderId); // リンク glLinkProgram(programId); glUseProgram(programId); return programId; } int main() { if( !glfwInit() ){ return -1; } GLFWwindow* window = glfwCreateWindow(g_width, g_height, "Simple", NULL, NULL); if (!window) { glfwTerminate(); return -1; } // モニタとの同期 glfwMakeContextCurrent(window); glfwSwapInterval(1); GLuint programId = crateShader(); // ゲームループ while (!glfwWindowShouldClose(window)) { // 画面の初期化 glClearColor(0.2f, 0.2f, 0.2f, 0.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glClearDepth(1.0); // モデルの描画 glDrawArrays(GL_TRIANGLES, 0, 3); // バッファの入れ替え glfwSwapBuffers(window); // Poll for and process events glfwPollEvents(); } glfwTerminate(); return 0; }
crateShader関数の中では、バーテックスシェーダとフラグメントシェーダを記述し、それをコンパイル&リンクしています。通常シェーダファイルはOpenGLのファイルとは別のファイルに記述するのですが、最初のうちはややこしいだけなので1つのファイルにまとめて記述しています。
このプログラムは実行しても、次のように画面には何も表示されません。これだけプログラムを書いたのに、まだ何も表示されないとは・・・・Unityと比べるとOpenGLは敷居が高いですね!
画面にポリゴンを表示するには今作ったシェーダにモデルの座標情報を渡す必要があります。ついに次回こそはポリゴンを表示しましょう。