syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

GLSLが使いにくい件について

(これは2011-09-20に書いた故OCNブログの記事を加筆修正したものです)

個人的にグラフィックス プログラミングが好きで、仕事でも趣味でもDirectXOpenGLを使っているんですが、OpenGLの不甲斐なさにそろそろ愛想が尽きてきました。
具体的にDirectX (Direct3D) と比べてOpenGLのどこがダメなのか、今回はそれぞれのシェーダー言語(シェーディング言語)であるHLSLとGLSLを比較しながら、OpenGLおよびGLSLの使いづらさを列挙していきましょう。

エフェクト

普通はHLSLもGLSLもシェーダーステージごとにプログラムのソースファイル(またはソースコード文字列)を分けてコンパイルしますが、HLSLというかDirect3Dでは「エフェクト」と呼ばれるフレームワークライブラリを使うと、頂点シェーダーもジオメトリ シェーダーもピクセル シェーダーもその他シェーダーもすべて単一のエフェクト ファイル中に記述できて、さらにテクニックやパスと呼ばれる仕組みを使うことで任意のシェーダーを組み合わせてグループ化し、好みのパイプラインを構築することが簡単にできるようになります。が、GLSLというかOpenGLにはそういう仕組みはありません。

NVIDIAのCgFXを使えばOpenGL環境でもDirect3Dエフェクト並みの便利さは享受できるんですが、Cgはすでに開発が終了していてコンピュート シェーダーすらサポートしていない状況なので今後は使わないほうが良いでしょう。Cgランタイム自体はこの記事を見る限り、一応AMD (ATI) のハードウェア上でも動くらしいです。現にPhotoshop CS5やLightWave 11.6ではCgが使われていますが、NVIDIA専用ではありません(Intelは知りません)。Cgランタイム自体はドライバーレベルの動作ではなく、HLSL/GLSLのコード ジェネレータみたいな位置付けのユーザーモード ライブラリになっている気がします。CgはHLSLやGLSLよりもパフォーマンスが出ないという話があるのですが、確かにそれだと手書きでガリガリ書いたコードに比べてフルパフォーマンスは引き出せないよね。PS3はCgが標準シェーダー言語らしいんですが、PS3GPUNVIDIAプラットフォームなので、専用ドライバーでなんとかしているのかもしれません。ちなみにCg Toolkitバージョン3.1時点では、Direct3D 10/11用のCgランタイムはD3DXランタイムとD3DCompilerランタイムに依存している(おそらくリフレクションやファイルからの実行時コンパイルを実現するために内部で使用されている)ので、アプリケーションに添付して再配布する場合は注意が必要です。なお、CgFXの後継としてクロスプラットフォームなエフェクト フレームワーク「nvFX」というのが開発されているのですが、コイツがこの先本当に使えるモノになるのかどうかちょっと不透明です。

余談:Direct3Dエフェクトの今後

Direct3Dエフェクトは使う側からすれば非常に便利な反面、いろいろと不遇な扱いを受けています。Direct3D 9の頃はコアAPIには含まれてなくてD3DXでの実装となっていました(Effects9)。Direct3D 10では晴れてエフェクトがコアAPIに取り込まれたのですが(Effects10)、Direct3D 11では再びD3DXでの実装に戻ってしまっています(Effects11)。その代わりEffects11はソースコードが公開されているので、カスタマイズは容易になっています。ただし、Effects11はこれまでのEffects9/10ユーザーに対する互換性提供のためのおまけ扱いのライブラリで、今後の使用は推奨されていません。おそらくDirect3D 12で追加される新しいシェーダープロファイルをサポートするコンパイラfxc.exeでも、Effects12はサポートされないでしょう。現にVisual Studio 2013/Windows SDK 8.1付属のfxc.exeではエフェクトファイルをコンパイルしようとすると、

FXC : warning X4717: Effects deprecated for D3DCompiler_47

という警告メッセージが表示されます。またfxc.exeは"/?"オプションを付けて起動するとヘルプが表示されるんですが、profileのリストの中にfx_4_0やfx_4_1、fx_5_0はすでに存在しません。MSは本気でエフェクトを抹殺する気らしいです。

2015-08-28追記:
Visual Studio 2015/Windows SDK 10付属のfxc.exeではまだエフェクトは完全廃止されておらず、エフェクトをコンパイルすること自体はできますが、後方互換のために一応残されているというレベルで、サポートされるのはfx_5_0までとなります。つまり、エフェクトではDirect3D 11.3で使用可能なシェーダーモデル5.1 (cs_5_1, ds_5_1, gs_5_1, hs_5_1, ps_5_1, vs_5_1) はサポートされず、fx_5_1は存在しません。VS 2013までと同様、エフェクトではシェーダーモデル5.0 (cs_5_0, ds_5_0, gs_5_0, hs_5_0, ps_5_0, vs_5_0) までを使用することができます。ちなみにDirect3D 12では、パイプラインステートオブジェクト (PSO) として事前に各種レンダリングステートやシェーダーステージを結合してしまう方式なので、エフェクトによるシェーダー設定管理は不要となります。もしDirect3D 12でエフェクトファイルのような階層構造フォーマットを使ったPSO管理を行ないたい場合、独自のエフェクトファイル仕様を定義・解析して、対応するPSOを構築するようなパーサーを書くとよいかもしれません。

Uniform変数

生のGLSLではuniform変数(≒グローバル定数)を複数のシェーダープログラム(glCreateProgram()で作成できるプログラム オブジェクト)で共有できないので、それぞれのシェーダープログラム単位ごとに変数定義して、さらにglUseProgram()を使ってホストプログラム側からシェーダープログラムをコンテキストにバインドした後、OpenGL APIを使ってシェーダープログラムごとの変数に対して値を逐一セットしてやる必要があります。これが実に面倒です。Direct3D/HLSLの場合、定数バッファの実体メモリはそれを作成したデバイス内で共有されるため、シェーダーステージごとのスロットに定数バッファを一度バインドすれば、その後異なるシェーダープログラム オブジェクトをバインドしても揮発したりせずそのまま使えます。頂点シェーダーとピクセル シェーダーとで同じ定数バッファのメモリを参照することもできます。SRVやUAVのスロットに関しても、Direct3D/HLSLはシェーダーステージごとにデバイス内で共有できるようになっています。
次にシンボル名の管理に関してですが、uniform変数名は頂点シェーダーとフラグメント シェーダーでネームバッティングしてもOKでリンクも成功するし、実際にリンク後はひとつの変数として扱われるらしく、glUniform~()でステージ間の共通値を設定することができるんですが、各シェーダーステージで異なる型を使うとおかしなことになるはずなので、この名前管理もしないといけないのが実に面倒です。
Direct3Dエフェクトでは、GLSLのuniform変数に近い仕組みとして「エフェクト変数」というものが存在します。エフェクト変数は内部で定数レジスタや定数バッファを使ったユーザーモードのコードでライブラリとして実装されている、おまけ扱いの機能ですが、前述のとおりエフェクトでは複数のシェーダーステージをまとめて記述できるので、エフェクト変数はuniform変数より管理しやすくなっています。
逆にOpenGL 3.1では、Direct3Dの定数バッファに近い仕組みとして「Uniform Buffer Object (UBO)」が追加されました。ただしDirect3Dの定数バッファとは違って、UBOはスロット数の下限や上限が規格で決まっていなくて、ハードウェア/ドライバー依存になっています。使うときはハードウェア/ドライバー仕様も把握しておく必要があるので注意しましょう。

グローバル定数をひとつひとつ書き換えていくエフェクト変数/uniform変数と、ブロックごとにまとめて書き換える定数バッファ/UBOのどちらが使いやすいかはケースバイケースになるかもしれませんが、非常に多数の定数を扱うような大規模なシェーダープログラムになってくると、やはり定数バッファ/UBOに軍配が上がるでしょう。

ゼロクリア

GLSLでは構造体をゼロクリアする構文が面倒です。

HLSLだと、やや変則的ながらも

struct MyStruct
{
  float4 Position;
  float3 Normal;
};

void Func()
{
  MyStruct st = (MyStruct)0;
}

という簡潔な構文で簡単にゼロクリアできるんですが、
GLSLだと、

struct MyStruct
{
  vec4 Position;
  vec3 Normal;
};

void Func()
{
  MyStruct st = MyStruct(vec4(0.0, 0.0, 0.0, 0.0), vec3(0.0, 0.0, 0.0));
}

としないとダメです。だらだらだらだら書きます。GLSLがCスタイルのキャストを一切許可しない(必ずコンストラクタ構文を使う必要がある)仕様になっていることが背景にあるんですが、実に面倒です。しかもGLSLではC/C++と違って整数リテラルから浮動小数点数への暗黙変換も公式サポートされていないので、vec4(0,0,0,0)と書くとコンパイルエラーになるドライバーもあるらしいです(特にモバイル環境で顕著)。vec4(0.0)という省略構文はあるのですが、せめて

MyStruct st = {0}; // C/C++ 互換風。

とか

MyStruct st = {}; // C++ 風。

みたいなゼロクリア用初期化子リスト構文もサポートして欲しいと思いませんか……

なお、OpenGL 4.2(GLSL 4.2)以降では初期化子リスト (initializer list) 構文もサポートされるようになったらしいです。つまりHLSL同様に、コンストラクタ構文も初期化子リストも両方サポートするようになりました。だったら最初から対応しておけと(以下略)

Typedef不在

GLSLにはHLSLと違ってtypedefがありません。自分はHLSLのfloat4/float3/float2, double4/double3/double2, uint4/uint3/uint2, int4/int3/int2などと比べて、GLSLのvec4/vec3/vec2, dvec4/dvec3/dvec2, uvec4/uvec3/uvec2, ivec4/ivec3/ivec2などは命名が気持ち悪くてセンスがないと思うので、できればHLSLに合わせてtypedefしたいんですが、肝心のtypedefがサポートされていないという始末。#defineを使わないといけないとかいつの時代の処理系ですか……

最適化によるシンボル除去

GLSLのuniform変数あるいはin/out変数(旧attribute/varying)へのロケーション インデックスを取得するとき、glGetUniformLocation()あるいはglGetAttribLocation()を使いますが、これらの変数が実際のシェーダーの出力結果に影響を及ぼさないとき(※単に「GLSLコード中で使われていないとき」ではない)、ドライバー側でシェーダーが最適化されて不要なシンボルが除去されるせいでこれらの関数の戻り値として-1(GL_INVALID_INDEX, Not Found)が返ってくる仕様になっています。個人的にこれが一番ひどいと感じます。シェーダーを書いている最中は試行錯誤することもあるので、uniform変数は残しておいたまま代わりに一時的にconst変数を使ったり、頂点ストリーム中のデータをあえて一時的に無視したりすることもあるんですが、このGLSLというかOpenGL実装系の腐った仕様により、そういう試行錯誤をするのがひどく難しくなっています。自分はそういうときの一時しのぎとして、使わないパラメータにはごく小さい浮動小数点数値を乗じてから出力値に加算するなどしてコード中にむりやり埋め込み、余計な最適化を抑制するアホな対処を行なっています。uniformはまだしも、in/outまでドライバー側でこんな余計なことされるとマジでやる気なくします。やっぱりGLSLめんどいです。できれば最適化を行なわないコンパイル オプションとか付けられないものですかね……

バイトコード

これまで述べてきたように、GLSLは弱点満載です。しかし、なにより最も痛い仕様といえば、GLSLにはバイトコード規格が存在しないこと。必ず実行時に毎回ドライバーにソースコード文字列を渡す必要があります。したがってHLSLみたいにオフライン コンパイラでプリコンパイルができません。コンパイル結果のバイナリを再利用する仕組みに関してはOpenGL 4.1で GL_ARB_get_program_binary として追加されましたが、ベンダー依存なのでただのキャッシュとしての意味しかありません。あとオフライン コンパイラの概念がないゆえに、GLSLでは標準で#includeがサポートされないのもなにげに痛いです*1。またOpenGLはマルチスレッドサポートも貧弱なので、時間のかかる多数のシェーダープログラムのコンパイルをサブスレッドにやらせて、生成されたオブジェクトをメインスレッドのOpenGLコンテキストで利用する、というようなごく当たり前の実装すら困難です。
2017-11-08追記:
OpenGL 4.6で、Vulkan同様のシェーダー中間表現SPIR-Vがようやく標準化されました。今後OpenGL 4.6を採用するアプリケーションがどのくらい出てくるか不明ですが、OpenGL 4.6正式対応のデバイスドライバーは現時点でまだどこもリリースしていないため、日の目を見るのはまたしばらく先になるでしょう。

まとめ

OpenGL/GLSLはなぜDirect3D/HLSLと比べてやたら使いにくいライブラリ・言語仕様になっているのでしょうか?
原因はいくつかあると思いますが、OpenGLが過去の互換性や古くさい設計思想を捨てられないでいることのほかに、OpenGL仕様策定に関わっている開発者のくだらないこだわり、すなわち

「俺たちのOpenGLは絶対にDirectX (Direct3D) とは違うものにしようぜ!(キリッ)」

がひとつの原因になっていると自分は考えています。OpenGL仕様に携わっている人は大半がアンチMicrosoft・アンチWindows・アンチDirectX派なので、とにかくDirectXと違う方向にしたいというおかしなこだわりだけで仕様や名称を決めてきたために、実際のアプリケーション開発のしやすさを徹底的に無視した作りになっています。最近はDirect3Dライクな機能を取り入れるなど、昔と比べるとかなり軟化してますが、かのジョン・カーマックも述べているとおりAPIとしての完成度・使いやすさはDirect3D (Direct3D 11) のほうが遥かに上でしょう。DirectXはOSメーカーが自分たちのプラットフォーム向けに完全独自設計しているだけあって、ときどき互換性を切り捨てて思い切った設計変更を行ないながらも、アプリケーション開発者のほうを向いて設計され、また統一感のあるAPIとして改良され続けていると感じます。ハードウェアメーカーではなく、ソフトウェアメーカーが主導権を握ることで良い結果を生み出している好例です。残念ながらこれは両方を実際に使い込んだことのある人間(≒DirectXを食わず嫌いせずきちんと使い込んだことのある人間)にしか分からないので、OpenGLしか使ったことがないという人はクロスプラットフォームや互換性という甘言にだまされていると思います。

2014-08-21追記:
SIGGRAPH 2014でOpenGLのAPI刷新がアナウンスされました。シェーダーバイトコード仕様とかマルチスレッドレンダリングとか、ようやくDirectX並みにまともな再設計を行なうらしいです。正直遅すぎです。DirectXではバイトコード仕様は2002年のDirect3D 9で、マルチスレッドレンダリングは2009年のDirect3D 11にてすでに実装されていました。OpenGLは本当に10年以上遅れています。コンピュータグラフィックスで10年といえば、化石となるには十分すぎる時間でしょう。

*1:#includeをサポートするARB拡張GL_ARB_shading_language_includeは一応存在しますが、HLSLと違って標準でサポートされないので、その存在価値は極めて薄いです。