◆◆ シェーダーで特殊効果 ◆◆
SFML2.5 非公式日本語翻訳
はじめに
シェーダーとは何か?
一種のプログラム言語です。グラフィックカード上で直接実行されます。
シェーダーを使うと、描画のプロセスを柔軟&シンプルに記述できます。
その柔軟&シンプルさときたら、OpenGL の既存の描画方法を上回るほどなのだ!
そういうわけでシェーダーは、通常の OpenGL の機能だけで実現するのは
(不可能ではないにしても)複雑になってしまうような特殊効果を記述するために使われます。
パーピクセル・ライティングとか、シャドウとか。
最近のグラフィックカードや新しいバージョンの OpenGL ではもう全面的にシェーダーがベースになっていて、
お馴染みの描画方法(いわゆる「固定機能パイプライン」)は非推奨になってます。
将来的には削除されることでしょう。
シェーダーを記述するには GLSL という言語を使います。
みんなの大好きな C言語にソックリな顔をしてます。
(ちなみに GLSL は OpenGL Shading Language の略)
シェーダーには2種類あります。
頂点シェーダーと、フラグメントシェーダー(またはピクセルシェーダー)です。
頂点シェーダーは頂点1つ1つに対する処理を行います。
フラグメントシェーダーは生成されたフラグメント(ピクセル)1つ1つに対する処理を行います。
どんな効果を実現したいかによって、どちらか片方だけのシェーダーを使ってもいいし、両方使うこともできます。
シェーダーの仕組みや使い方を理解するには、レンダリングパイプラインのイロハを理解する必要があります。
それと、GLSLプログラミングのことも。
道は遠いですが、いいチュートリアルやサンプルを見つけてね!
SFML の SDK の中にも シェーダーのサンプルがあるから、よかったら見てみてね。
このチュートリアルでは、あくまでも SFML 側の機能に焦点を当てます。
つまり、シェーダーを読み込んで使う方法です。シェーダーそのものの書き方については、触れません。
シェーダーを読み込む
SFML でシェーダーを表すクラスは
sf::Shader です。
これ1つで頂点シェーダーとフラグメントシェーダーを両方扱えます。
sf::Shader クラスのインスタンスは両シェーダーのコンビネーションです(片方しか使わないなら片方だけでも OK)。
シェーダーが一般的になってきているとは言っても、まだシェーダーをサポートしてないグラフィックカードがあるかもしれません。
なので、プログラムの中で最初にするのは、シェーダーが使えるかどうかのチェックです:
if (!sf::Shader::isAvailable())
{
// シェーダーはサポートされてません……
}
sf::Shader::isAvailable() が false になる場合、シェーダーは使えません。
シェーダーを読み込むには HDD上のファイルを読み込むのが基本です。
loadFromFile() 関数を使います。
sf::Shader shader;
// 頂点シェーダーだけを読み込む
if (!shader.loadFromFile("vertex_shader.vert", sf::Shader::Vertex))
{
// error...
}
// フラグメントシェーダーだけを読み込む
if (!shader.loadFromFile("fragment_shader.frag", sf::Shader::Fragment))
{
// error...
}
// 両方のシェーダーを読み込む
if (!shader.loadFromFile("vertex_shader.vert", "fragment_shader.frag"))
{
// error...
}
シェーダーは単なるテキストファイルです(C++ などのプログラムと同じように)。
なので、拡張子はあんまり関係ありません。好きなようにつけて OK です。省略したって OK です。
".vert" や ".frag" というのは単に、ありそうな例というだけです。
この loadFromFile関数なんだけど、ときどき、よくわかんない理由で失敗してしまいます。
そんなときは、まず、エラーメッセージをチェックせよ。コンソールに出力されてます。
さて、なんて言ってるでしょう? 「unable to open file」? そんなときは、カレントディレクトリを確認だ。
カレントディレクトリっていうのは、キミのアプリケーションがファイルを読み込むときの、
相対パスの出発点になるフォルダのことです。 さて、キミの思ってる通りのフォルダかな?
アプリケーションをデスクトップ環境で起動しているときは、
そのアプリケーションが置いてあるフォルダがカレントディレクトリなのだけど、
IDE(Visual Studio とか、Code::Blocksとか)から起動してるときは、
そのプロジェクトのフォルダになってるかも。プロジェクトの設定でカンタンに変更できるはず。
訳注:「
スプライトとテクスチャ」の loadFromFileに関する注意書きと同じ文面です(原文が)。
シェーダーは文字列で直接記述することもできます。loadFromMemory() 関数を使います。
シェーダーをソースコードの中に埋め込んでおきたいときには、この読み込み方法を使うとよいです。
const std::string vertexShader = \
"void main()" \
"{" \
" ..." \
"}";
const std::string fragmentShader = \
"void main()" \
"{" \
" ..." \
"}";
// 頂点シェーダーだけを読み込む
if (!shader.loadFromMemory(vertexShader, sf::Shader::Vertex))
{
// error...
}
// フラグメントシェーダーだけを読み込む
if (!shader.loadFromMemory(fragmentShader, sf::Shader::Fragment))
{
// error...
}
// 両シェーダーを読み込む
if (!shader.loadFromMemory(vertexShader, fragmentShader))
{
// error...
}
そして最後に もう1つの読み込み方。ストリームから読み込むこともできます。
使う関数は loadFromStream() です。
SFML の他のリソースクラスと同じ要領ですね。
もし読み込みに失敗するときは、標準出力(コンソール)に出ているエラーメッセージを見てください。
GLSL からの詳しいレポートが表示されてるはずです。
シェーダーを使う
使うのは簡単です。
draw() 関数の引数に渡すだけです。
// 通常はこうするところを……
window.draw ( sprite );
// 第二引数にシェーダーを指定する
window.draw ( sprite, &shader );
シェーダーに変数を渡す
他のプログラム言語と同じように、シェーダーもパラメータを持つことができます。
パラメータの内容次第で、描画のたびに違う結果になる、というわけです。
パラメータはシェーダーの内部で、グローバルな変数として宣言します。いわゆる「ユニフォーム変数」です。
uniform float myvar;
void main()
{
// myvar を使った処理
}
ユニフォーム変数は C++ 側からセットします。
sf::Shader クラスに、変数の型ごとに setUniform() 関数がたくさん用意されてるので、使い分けてください。
shader.setUniform ( "myvar", 5.0f );
以下、SFML が setUniform でサポートしている 型の一覧です:
- float (GLSL での float型)
- 2 floats または sf::Vector2f (GLSL での vec2型)
- 3 floats または sf::Vector3f (GLSL での vec3型)
- 4 floats (GLSL での vec4型)
- sf::Color (GLSL での vec4型)
- sf::Transform (GLSL での mat4型)
- sf::Texture (GLSL での sampler2D型)
GLSL のコンパイラは、使われていない変数を見つけると省略してコンパイルします
(ここでの「使われてない」というのは、頂点やピクセルの計算に関係しない、という意味)。
そういう場合、setUniform() の実行時に "Failed to find variable "xxx" in shader" みたいなメッセージが出ることがありますが、
驚かないで。
最小限のシェーダー
ここでは GLSL の書き方のお勉強はしないのですけど、
でも、SFML から シェーダーにどんなインプットが行われていて、プログラマとして何をすべきなのかは
覚えておかねばならないのであります。
頂点シェーダー
SFML には頂点を表す
sf::Vertex という構造体があります。
この頂点は「2D座標」と「色」と「テクスチャ座標」を持ってます。
まさにこれらが、頂点シェーダーにインプットされるデータです。
頂点シェーダ側で「gl_Vertex」「gl_Color」「gl_MultiTexCoord0」といった変数がビルトインされていて、
それらのデータを受け取るようになっています(プログラマが宣言する必要はない)。
void main()
{
// 頂点の座標をトランスフォーム
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
// テクスチャ座標をトランスフォーム
gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0;
// 頂点色をフラグメントシェーダに渡す
gl_FrontColor = gl_Color;
}
座標は ModelView行列と射影変換行列で変換される必要があります。
オブジェクトを現在のビュー座標系に位置づける変換行列ということです。
テクスチャ座標はテクスチャ行列で変換される必要があります
(通常、この行列はプログラマが気にする必要はありません。SFML の内部実装の裏話です)。
色はフラグメントシェーダー側にそのまま渡せば OK です。
もちろん、必要なければ色やテクスチャ座標は無視して構いません。
これらの変数がグラフィックカードでジオメトリ内のプリミティブに適用され、
フラグメントシェーダーに渡されます。
フラグメントシェーダー
フラグメントシェーダーも似たような感じです。
シェーダーが受け取るデータは「テクスチャ座標」と「生成されたフラグメント(≒ピクセル)の色」です。
「座標」はありません。
ここまで処理が進んだら、グラフィックカードはすでにフラグメントの最終的な座標を計算し終えているからです。
それと、テクスチャを貼っている場合は、テクスチャも必要です。
uniform sampler2D texture;
void main()
{
// テクスチャ内のピクセル情報を取得
vec4 pixel = texture2D(texture, gl_TexCoord[0].xy);
// 色を乗算
gl_FragColor = gl_Color * pixel;
}
テクスチャは現在のテクスチャが自動で適用される、といいのですが、自動では適用されません。
C++の側から明示的に設定してあげる必要があります。
でも、2Dオブジェクトごとに別々のテクスチャがありますよね?
それを描画のたびに毎回取得して、シェーダーに渡すのはプログラマにとって、とても大変なことです。
そんなわけで! あなたの代わりにそのお仕事をしてくれる 特別な setUniform() 関数を紹介します。
いやー、SFMLって気が利きますね。
shader.setUniform("texture", sf::Shader::CurrentTexture);
これで、描画中のオブジェクトのテクスチャが、描画のたびに自動的に、シェーダーの変数に設定されるようになります。
新たなオブジェクトが描画されるたびに SFML は、対応するシェーダーの変数を更新してくれます。
実際に動いているシェーダーの素敵なサンプルが見たいときには、
SFML SDK の "examples" フォルダを覗いてみてね。
sf::Shader を OpenGL のコードで使う
描画処理に SFML の2Dオブジェクトを使わずに OpenGL を使っているお友達もいるかもしれませんね。
そんなあなたにも、やっぱり sf::Shader は強い味方。
sf::Shader は OpenGLのシェーダーのラッパーになって、OpenGL のコードとあなたを仲良しにしてくれます。
sf::Shader を有効にして描画するには、
スタティック関数の bind() をコールします(glUseProgram に該当)。
sf::Shader shader;
...
// シェーダーをバインド
sf::Shader::bind(&shader);
/*
……あなたの OpenGL プログラミング……
*/
// シェーダーのバインドを解除
sf::Shader::bind(NULL);