OpenGLの話 第2回
GLSLコンパイラ 13/12/08 up
今回はOpenGL用のシェーダ言語であるGLSLのコンパイラを作ってみました。
一応、ユーザ指定のDefine定義とインクルードファイルに対応しています。
インクルードファイルは単純に展開するだけなので、インクルードガードはしっかりやっておく必要があります。
#pragma onceのインクルードガードは対応してません。#ifndef~#endifで対応してください。
GLSLはDirectXのHLSLと同様にC言語ライクなシェーダ言語です。
予約語に違いはありますが、基本的な考え方は一緒だと思っていいでしょう。
例えば、HLSLではfloat4ですが、GLSLではvec4だったり、エントリーポイントはmainオンリーだったりします。
また、インクルードファイルに対応していません。
しかしながら#defineや#if~#endif各種は対応しているようです。すべての環境で対応しているかどうかはわかりませんが…
入出力部分は結構違いますが、この辺はまだ自分でもよくわかってないところがあります。
そのあたりは使っていけばおいおい覚えていけるでしょう。
HLSLユーザとして若干面倒な部分としてはオフラインコンパイラが存在していない点です。
HLSLではfxc.exeというオフラインコンパイラがあり、VisualStudioもこのコンパイラを利用してオフラインコンパイルしてくれますが、GLSLにはこのようなツールが存在しません。
これはDirectXとOpenGLの違いに由来しています。
DirectXではライブラリの内部実装をMicrosoftが作成しています。
ここでは各GPUのドライバの機能を呼び出しているものと思われますが、言ってしまえばDirectXはユーザとドライバを仲介するライブラリということになります。
これはHLSLでも同様で、HLSLのバイナリというのは実はこれすら中間ファイルにすぎず、ドライバはこのHLSLバイナリから自分たちが使用可能なシェーダ言語に変換しているわけです。
2度手間ではあるのですが、ユーザからすればオフラインでビルドが可能かどうかチェックでき、シェーダプロファイルが対応していればどのGPUでも使えることが保証されているという利点があります。
GLSLは基本的にGPUベンダーがライブラリを提供している関係で、シェーダバイナリも直接そのGPU用のものにビルドされます。
そのため、同一環境(同一OS、同一GPU、同一ドライバ)ではバイナリの使いまわしも可能なのですが、そうでなければバイナリを使いまわせるという保証はありません。
OpenGLでは3.2くらいからシェーダバイナリに対応していますが、使い道としては初回起動時にコンパイルしたシェーダを保存しておいてキャッシュにするくらいしかなさそうです。
もちろん、GPUやドライバが変化したなら作り直す方が安全でしょう。
では、まず今回のシェーダを紹介します。
VSTest.glsl
#version 420 core
#include "calc_color.glsl"
#ifndef COLOR_CALC_TYPE
# define COLOR_CALC_TYPE 0
#endif
layout(location = 0) in vec4 viPosition;
out vec4 voColor;
void main()
{
gl_Position = viPosition;
#if COLOR_CALC_TYPE == 0
voColor = addColor(vec4(0.5, 0.5, 0.5, 1.0), vec4(0.2, 0.5, 0.0, 0.0));
#else
voColor = subColor(vec4(0.5, 0.5, 0.5, 1.0), vec4(0.2, 0.5, 0.0, 0.0));
#endif
}
// EOF
calc_color.glsl
#ifndef INCLUDE_CALC_COLOR_GLSL
#define INCLUDE_CALC_COLOR_GLSL
vec4 addColor(vec4 c0, vec4 c1)
{
return c0 + c1;
}
vec4 subColor(vec4 c0, vec4 c1)
{
return c0 - c1;
}
#endif // INCLUDE_CALC_COLOR_GLSL
// EOF
COLOR_CALC_TYPEが0の場合は頂点カラーとして定数値の加算、1の場合は減算を出力します。
ピクセルシェーダ(OpenGLではフラグメントシェーダという)はその値をそのまま出力するだけです。
頂点シェーダの最初に#versionという記述があります。
これはシェーダのバージョンを意味していて、ここではバージョン4.2を利用してコンパイルするように指示しています。
なお、記述がない場合は1.1でコンパイルされるようになりますが、GL3.0以降はシェーダバージョン1.5以上でなければ使えないようです。
HLSLのシェーダプロファイルと同様のものと考えればいいですし、GL3.0以降は使用するGLのバージョンとシェーダバージョンは同一でOKです。
viPosition, voColorに付属しているin/out修飾子はシェーダへの入力/シェーダからの出力を意味します。
頂点シェーダの場合、inは頂点入力、outはラスタライズステージへの頂点出力となり、
ピクセルシェーダの場合はinがラスタライズステージからの各ピクセルの頂点補間情報、outがフレームバッファへの出力となります。
layout修飾子は入出力情報のレイアウト情報で、()内に各種情報を書き込むことで入力元や出力先を固定することができます。
頂点入力の場合、locationの数値を固定することで頂点バッファの属性位置を固定できます。
固定されていない場合、プログラムサイドでglBindAttribLocation()関数を使ってバインドしてやる必要があります。
バインドしなければ自動的に0から番号が割り振られるのかな?よくわかりませんが、指定してやった方が安全です。
ピクセルシェーダでは出力バッファ番号を指定できます。
今回作成したGLSLコンパイラはSTLをかなり使っています。
勝手にメモリを取られるのは嫌だ、という方はアロケータを利用するなり自前のコンテナを利用するなりで改造してください。
インクルードファイルの読み込みもC言語標準のファイル入出力命令を利用しているので、改造した方がいろいろ困らないと思います。
以下がGLSLコンパイラクラスのインターフェースです。
// GLSLコンパイラ
class GLSLCompiler
{
public:
GLSLCompiler();
~GLSLCompiler();
// インクルードパスを追加する
int AddIncludePath(const std::string& path);
int AddIncludePath(const char* path);
// インクルードパスをクリアする
void ClearIncludePaths();
// 定義を追加する
int AddDefine(const std::string& def, const std::string& val);
int AddDefine(const char* def, const char* val);
// 定義をクリアする
void ClearDefines();
// ファイルからGLSLをコンパイルする
bool CompileFromFile(GLuint shader, const char* filename);
// 文字列からGLSLをコンパイルする
bool CompileFromString(GLuint shader, const std::string& source);
// GLSLで#includeディレクティブが使用できるかどうか調べる
static bool HasIncludeARB();
}; // class GLSLCompiler
AddIncludePath()でインクルードファイルを読み込むパスを追加します。複数指定可能です。
ClearIncludePaths()は登録済みのすべてのインクルードパスを破棄します。
AddDefine()はプログラム中で指定可能なDefine定義を追加します。定義の名前と文字列を指定します。
ClearDefines()で登録済みのすべての定義をクリアします。
CompileFromFile()は指定ファイル名のGLSLテキストファイルを読み込んでコンパイルします。
CompileFromString()は指定した文字列からコンパイルします。CompileFromFile()は内部でこのメソッドを読んでいます。
HasIncludeARB()はおまけ機能です。
GLSLはインクルードファイルに対応していませんが、実は拡張機能としては存在しています。
GL_ARB_shading_language_include という拡張がそれになるのですが、対応している環境はそれほど多くないようです。
元はNVIDIA拡張だったらしいので、GeForce系は対応しているかもしれませんが、Radeonは対応していませんでした。
まあ、対応していない環境の方が多いようなので、使わない方が無難だと思います。
なお、GLSLコンパイラはSTLのvectorやstringを用いていますが、regexとunique_ptrも用いています。
現在の大半のコンパイラは対応しているはずですが、対応していないコンパイラを使用する場合はboostなどで置き換えることを推奨します。
unique_ptrは使わなくてもなんとかなりますが、regexは使わないとインクルードファイルの対応ができないので注意してください。
使い方のサンプルは以下です。
GLSLCompiler compiler;
compiler.AddIncludePath("data/shader");
compiler.AddIncludePath("data/shader/include");
compiler.AddDefine("COLOR_CALC_TYPE", "0");
g_VShader0 = glCreateShader(GL_VERTEX_SHADER);
if (!compiler.CompileFromFile(g_VShader0, "data/shader/VSTest.glsl"))
{
return false;
}
compiler.ClearDefines();
compiler.AddDefine("COLOR_CALC_TYPE", "1");
g_VShader1 = glCreateShader(GL_VERTEX_SHADER);
if (!compiler.CompileFromFile(g_VShader1, "data/shader/VSTest.glsl"))
{
return false;
}
COLOR_CALC_TYPEを0と1で同じシェーダファイルを1回ずつビルドしています。
サンプルプログラムはテンキーの0と1でシェーダの切り替えを行っています。
0番が加算したバージョン、1番が減算したバージョンとなります。
コンパイルするシェーダがどの種類のシェーダか(頂点シェーダなのかピクセルシェーダなのか)は、コンパイル命令の第1引数で渡すシェーダIDがどの種類で作られたかで決定されます。
glCreateShader()の引数でピクセルシェーダ(フラグメントシェーダ)を渡しているにもかかわらず頂点シェーダをコンパイルしたりするとコンパイルエラーが発生します。
シェーダファイルやIDがどの種類なのかはコンパイラ側はチェックしないので注意してください。
ではサンプルです。
この程度のサンプルを作成するならこの程度のコンパイラでも十分じゃないかと思いますが、ライブラリに組み込むならもっとしっかり作ってやる必要があるでしょう。
あと、今回は循環参照対策としてインクルードのネストを16深度程度で切り捨てるようにしています。
シェーダプログラム程度であればそれほど深いインクルードはしないんじゃないかと思いますが、足りない場合は増やしてください。
次回はlibpngでも利用してテクスチャ読み込みでもやってみましょうかね?
こうやって最低限のものを作ってかないとサンプル作成すらままならないですからねぇ…