C-WebGL: Vulkan上でUniformとAttributeを実装する方法を考える会
... いや一応本業なんだけどライブラリ(やゲームエンジン)が普段やってくれている事を手でやろうとするとなかなか難しい。
シェーダー、つまりGPU上で動作するプログラムにパラメーターを渡す方法はいくつかある。WebGL1(= OpenGL ES2)では、
- Uniform 。フラグメントシェーダとバーテックスシェーダの両方で使用できる、テクスチャ番号やその他数値を渡すための仕組。
- Attribute 。バーテックスシェーダーに頂点位置や頂点色等の属性を渡すための仕組。
- Varying 。バーテックスシェーダーからフラグメントシェーダーにデータを渡すための仕組。
の3種ある。このうち varying はシェーダ間の通信にしか使用されずWebGLのAPI上では出てこないので今回は無視できる。
昔(DirectX9くらい)のGPUでは、これらをGPU側のレジスタに設定する形を取っていたので、OpenGLのようなAPIではかなり抽象化した形で扱われていた。しかし、プログラマブルシェーダの時代になって以降段々と実装が収斂してきたため、Vulkanではちょっと簡略化されている。
特にUniformは、OpenGLで言うところのUniform Buffer Object、DirectXで言うところの定数バッファと呼ばれる構造体にデータを積めてメモリ上に置くルールになっている。シェーダが扱うデータの量が増えてきて、GPUレジスタをちまちま設定するコストがバカにならなくなってきたというのが背景にある。
Attribute
GPUは大量の頂点で構成されたデータを入力する必要があるため、頂点の入力のためには専用のサポートが存在する(テクスチャのアクセスがすごく抽象化されているのと一緒)。
WebGL1の場合、 1) vertexAttrib
メソッド群で定数を設定する か、 2) vertexAttribPointer
で配列に対して stride と型を設定する の2通りの手法が取れる。
定数
たぶん直接的な方法は存在しないのでエミュレートする必要がある (本当。。?) AttributeDivisorは拡張で存在するので、それをelement数に伸ばして1要素のバッファを与えればできるかもしれないけど。。
これはANGLEがどうやってるか実験してみた方が良いかな。 → 実験した。 stride
を 0 に設定したバッファを指定するという至極単純な解法だった。。
配列
WebGLの vertexAttribPointer
メソッドは
void vertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, GLintptr offset)
のように定義される。indexはシェーダ側を名前でクエリすることで得られる。
offsetは VkVertexInputAttributeDescription
、sizeとtypeもこの構造体のformatとして設定することになる。strideの設定は VkVertexInputBindingDescription
の内部で可能。
... normalizedは。。?formatで normalizedなtypeを渡す( VK_FORMAT_R8G8B8A8_UNORM
とか) 。。だと思う。
個々の配列はlocationで識別される。WebGL1ではシェーダ側でlocationを振る方法は用意されていないので、コンパイルしたシェーダを書き換えてlocationをC-WebGL側でアサインしなければならない。
Uniform
WebGL1におけるUniformはテクスチャとデータに大別できる。
データ
これはまぁチュートリアルの通りで良いな。。
バッファを用意してbindし、事前に用意しておいたdescriptor setに突っ込めばOK。どちらかというと、これが成立するようにシェーダを書き換える方が難しい。。(WebGLのシェーダには当然この辺の情報は存在しないので良い感じに割り当ててやる必要がある)
テクスチャ
VulkanはWebGL2のようにsampler objectが別に存在するため、 それをdescriptorに突っ込みbindする ことになる。
リフレクション
GPUプログラミングでは何らかの形でのリフレクションが必須となる。 ...パフォーマンスAPIなのにコスト高そうなリフレクションってどうなんだよという気もするが、実際この辺は長いこと問題になっている。
WebGL1では、シェーダのパラメーターは全て名前で引く必要がある。Vulkanでは名前で引く必要があるのはエントリポイント(= main
) だけで、シェーダ間の入出力はlocationを合わせることで行う。
バッファ類、つまりGPUからフェッチする必要のある頂点やインデックス、テクスチャはデスクリプタにバインドする必要がある。このデスクリプタはbinding番号で識別される。これらbindingやlocationの付与はWebGL1のESSLではユーザ側で行えないため、 LinkProgram
のタイミングで実装側で暗黙に番号をアサインしてやる必要がある。
C-WebGLとしてやる必要があるのは:
- (
CompileShader
のタイミングで、) glslangを使用してESSLコードをSPIR-Vに変換する - SPIR-Vをスキャンしてアタッチされた頂点シェーダーやフラグメントシェーダーの attribute 、 uniform 、 varying を列挙する
- 各シェーダーのuniform群をマージし、構造体を生成する (同名のuniformがある場合は単一のインスタンスになる)
- 全てのエントリにbindingをアサインし、attributeとvaryingにはlocationもアサインする
- 各シェーダのSPIR-Vを適当にパッチする
これらの過程でアサインされたbindingやlocationは、後続の bindAttribLocation
とか uniform
の呼出しでも使用することになるので保存しておく必要がある。
↑の 3 で生成する構造体のレイアウトは一応 std140
という標準のABIがあり、特にWebGL2ではそちらの使用を強制している。