Uniform Buffer Object (UBO)

実行結果

今回のサンプルの実行結果

uniform 変数をバッファに格納

前回は、GLSL ES 3.0 より利用することが可能となったビルトイン変数である gl_VertexIDgl_InstanceID について扱いました。

頂点シェーダでのみ利用可能なこれらの変数は、従来の WebGL 1.0 で使われていた GLSL では実現できなかった、頂点のインデックスの参照などを行うことができる変数です。どんなときでも欠かさず利用するというようなものではありませんが、使いどころはいろいろあると思いますので、ぜひがんばって使いこなせるようにトライしてみてください。

さて今回ですが Uniform Buffer ObjectUBO)に挑戦してみましょう。

WebGL には、VBO や IBO など、これまでにもたくさんの「バッファ」と名の付くオブジェクトが登場してきましたが、今回紹介する UBO もそんなバッファの種類のうちのひとつです。WebGL 2.0 になって利用できるようになった新しいバッファタイプですね。

その名前を見れば、これがどういうものかはなんとなく想像がつくのではないかなと思いますが、UBO を使えば uniform 変数の情報をバッファオブジェクトとしてバインドし、GPU へと送り込むことが可能になります。これによるメリットはいろいろ考えられますが、代表的なメリットを挙げるとすれば 複数のプログラムで uniform 変数を共有 できることなどが挙げられます。

レンダリングの度に大量の uniform 変数をシェーダへ送る必要がある大規模な実装であれば、UBO を使うことによって、単体のオブジェクトをバインドするという単純な操作で、複数のプログラムオブジェクトに同様の情報をプッシュすることができるようになります。uniform 変数のプッシュはひとつひとつを見れば一瞬で終わるような処理ではありますが、これがプログラムオブジェクトを切り替える度に大量に行われるとなれば、それなりに大きなオーバーヘッドになります。

Uniform Buffer Object を利用することで、そのような処理を最適化していくことができるわけですね。

利用するための手順は、WebGL のご多分に漏れず、フロントエンド的な視点で見ると非常に冗長に感じる部分も多いかと思います。ひとつひとつ丁寧に解説していきますので、じっくりと見ていきましょう。

バッファにすることで uniform 変数を抽象化

今回の UBO について考える前に、まずは従来からある VBO や IBO を思い出しながら、バッファがどのように扱われていたのかおさらいしてみましょう。

たとえば一番利用頻度が高いと思われる VBO は、Vertex Buffer Object の名が示すとおり頂点に関する情報を扱うためのバッファでしたね。

WebGL しか利用したことがないと信じられないという方もいるかもしれませんが、古い実装の OpenGL には、そもそも VBO という概念自体が存在しませんでした。

つまりかつては、CPU の管理するメモリ領域から GPU 上のメモリ領域に対して、直接頂点の情報を流し込むことで頂点を描画していたわけです。一般に、CPU と GPU という異なるハードウェア間での情報のやりとりは処理時間的に不利が大きくなります。この問題を解消するために、VBO と呼ばれる頂点の情報を格納したバッファという概念が生まれました。VBO は GPU 上に初めから存在します。CPU 側にあるアプリケーションは、VBO をバインドするというステップを経てこれに干渉することができるようになり、効率的にレンダリングが行えるようになったわけです。

IBO も、インデックスを扱うという意味で VBO と役割は違っていますが、仕組みとしては同じです。そして今回のテーマである UBO も、やはりこれと同様の考え方から生まれたバッファオブジェクトだと言えるでしょう。uniform 変数としてシェーダ内で利用するデータも、バッファオブジェクトにしてしまい、VBO などと同様に扱うことができるようにしてしまおうというわけです。※このあたり若干自信ないところもあるので、詳しい方がいらっしゃったら教えてほしいです。

頂点のモデルデータは、どちらかというと、描画の途中でその中身が変化することは少ないと言えます。ですから多くの場合、VBO や IBO を生成するときは gl.STATIC_DRAW などの定数を指定してバッファを静的なものとして生成します。しかし uniform 変数は大抵の場合、頻繁に更新・変更されることが多いと思います。ですから UBO としてバッファを生成しても、それが頻繁にアップデートされる可能性なども考慮してやる必要があるでしょう。

また、UBO を使っている場合は、異なるプログラム間でバッファを共有するということも可能です。uniform 変数がひとつやふたつ程度なら、そこまでパフォーマンスに対しての影響が大きくなることはありませんが、これも変数の数が増えれば増えるほど、毎フレームシェーダへ送る情報量がかさばっていくことになります。ここで UBO のように uniform 変数がひとつのバッファとして集約した状態になっていれば、プログラムオブジェクトを切り替えた際にも、効率よくデータを転送できるという寸法ですね。

必然、あまり大規模でないアプリケーションでは、むしろ利用するための初期化や準備の工数のほうが多くなる場合もあるかもしれません。そこは、自身の実装内容に合わせて、適宜選択していきましょう。

uniform ブロック

さて、それでは具体的に Uniform Buffer Object を使う方法について見ていきましょう。

まず大前提として、UBO を利用するためには WebGL の実装だけでなく、GLSL の記述についても UBO を使うことを考慮した方法に合わせてやる必要がある、ということを理解しておきましょう。

Uniform 変数を複数まとめ(単体でも可能だが意味は薄い)、それをひとつのバッファオブジェクトとして扱うのが Uniform Buffer Object の考え方なので、シェーダの記述についてもそれに合わせた記述を行うわけです。感覚としては一種の構造体のようなものを定義して、それを WebGL アプリケーションの実装側から捕捉します。

従来の uniform 変数を利用する方法では、シェーダ内で定義した uniform 変数のロケーション情報を事前に取得し、それを指定した上で何かしらの情報をシェーダへ送り込んでいましたよね。この、シェーダからロケーションを取って、それを元にデータを紐付けていくという流れそのものは、UBO を使う場合も同じです。

それでは、上記の uniform 変数がシェーダに送られていくまでの流れをイメージしながら、まず最初に GLSL のほうから見てみましょう。

Uniform Buffer Object を利用する場合のシェーダの記述例

#version 300 es
layout (location = 0) in vec3 position;

layout (std140) uniform matrix {
    mat4 mvp;
} mat;

uniform float scale;

void main(){
    gl_Position = mat.mvp * vec4(position * scale, 1.0);
}

上記は、非常にシンプルな頂点シェーダの記述例です。

ポイントになるのは、attribute 変数を定義している部分の下にある layout から始まる定義文。まるで構造体の定義のように、括弧で区切られたブロックの中に変数定義が書かれているのがわかるかと思います。

まず最初のところで layout (std140) uniform と書かれている部分がありますね。これが uniform ブロック の定義を行うためのステートメントで、それに続く matrix が、この uniform ブロックに付けられている名前になります。また、閉じ括弧の後ろに mat と書かれているのがわかるでしょうか。これはこの uniform ブロックのインスタンス名を表していて、続く main 関数のなかでインスタンスが使われているのがわかると思います。

uniform ブロックのインスタンスを使った記述

gl_Position = mat.mvp * vec4(position * scale, 1.0);

Uniform Buffer Object に関連する部分は、最初は非常に用語などが紛らわしく感じられると思うので、ここは落ち着いて、ひとつひとつ確実に理解していきましょう。

まず uniform ブロックをシェーダ内で記述する際には、上記で示したように uniform xxxx {} という構造体の記述を行うような構文を使います。これが基本です。そしてこのブロックの閉じ括弧の後ろには、この uniform ブロックのインスタンス名を指定することができ、インスタンス名を指定した場合には、uniform ブロック内の要素にアクセスする際に、JavaScript のプロパティ参照のような感じで、ピリオドを使って名前を記述する形になります。上記の記述例で言えば mat.mvp の部分がそれに該当しますね。

インスタンス名は必ずしも必須ではないので、そこは自分がわかりやすい方法を使えばいいでしょう。

また、uniform ブロックの定義の最初にあった layout (std140) の記述、実はこれも GLSL の構文としてのルール上は必須ではありません。

しかし個人的には、これを使った uniform ブロックの記述を推奨します。

それはどうしてか? そもそも std140 ってなんやねん! という疑問が湧いてきますね。

少し記述方法の説明からは逸れますが、理解を深めるために、先にこの std140 について詳しく説明することにします。

layout を決める std140 とは

GLSL ES 3.0 から利用することができるようになった layout 修飾子は、以前のテキストで解説しましたね。

この修飾子を使うと、本来は処理系によって決められるロケーション番号を、実装者が任意に指定できるようになります。今回の std140 も、やはりこのあたりに関係があります。

そもそも、OpenGL などの実装は従来から C++ などのネイティブな言語が使われていて、メモリアロケーションなどを厳密にきっちりと行ってやる必要があります。型という概念が存在しない JavaScript とは違い、ひとつの変数が何バイトのメモリを使うのかなど、しっかりと考えなくてはいけないわけですね。

GLSL の内部で利用される変数にも同様のことが言え、ハードウェアや、その上で動くドライバなど、処理系によって GLSL 内部の変数がどれくらいのビット長になるのかは本来データ型ごと、あるいは実行環境ごとに異なるものになります。ですから、より厳密な手段を用いる場合は 事前に uniform 変数のビット長やレイアウトを取得 した上で、それに合わせて CPU 側でもデータを構築してやる必要があるんですね。

Uniform Buffer Object に格納するデータを用意する際には、当然ながらこの uniform 変数ひとつあたりのビット長には気を配らなければなりません。しかしこれは、実行環境に依存しない複雑なロジックを構築しなければならないということでもあり、なかなか冗長な準備してやらなくてはなりません。

そこでこのような複雑で冗長な処理を簡易化するために登場するのが、他ならぬ std140 です。

この std140 の「std」は、これは恐らくですが Standard の略だと思います。そして 140 は、GLSL のバージョン 1.4 を表しています。つまりどういうことかというと、この std140 が意味するところは、どのような実行環境でも変化しないスタンダードなレイアウトを使う、ということになりますね。

std140 でどのようにメモリの使用量が決まるのかは、仕様書などを見ていくと非常に細かく記載があります。おおよそ vec4 ひとつあたりで 16byte を使うような定義になっています。ですが統一化のために小さなデメリットには目をつぶるという側面もあり、たとえば vec3 型は vec4 と同じ 16byte を利用することになっています。わずかなビットの無駄遣いすら許されない! というような状況であれば、std140 を使って楽をせず、メモリアロケーションの効率を優先させなければならないということも、場合としてはあり得るわけですね。

packed, shared, std140

std140 以外にも、GLSL の文法上は packedshared という別の指定が行えるようになっています。

これらの指定の違いによって、uniform ブロック内の変数がどのように扱われるのかが変化します。たとえば packed では、処理系によって uniform 変数のレイアウトが決められた上、従来と同じように「未使用の uniform 変数」は削除されます。

UBO は異なるシェーダ(プログラムオブジェクト)間でバッファを共有できる仕組みですが、一方のシェーダでは利用されていても、もう一方のシェーダでは利用されていない、といった uniform 変数が出てきたときに、存在自体が消されている可能性があるというのはちょっと怖いですね。一方で shared を指定した場合には、やはり同様に処理系によってレイアウトが決まりますが、未使用の uniform 変数も削除されずに固定のレイアウトになります。

std140 の場合も、未使用であっても uniform 変数が勝手に削除されることはありません。また、上記でも書いたようにレイアウトはクロスプラットフォーム用に固定されたものになるので、別途アプリケーション側からこれらの情報を取得する調査系のメソッド呼び出しを省略することができます。

自身の記述したシェーダや、あるいは実装内容に合わせて、これらの指定方法の違いを理解した上で使う必要があるわけですね。

JavaScript 側での実装

さて続いては、先述した GLSL の記述に合わせた、JavaScript 側の実装について見ていきましょう。

JavaScript 側の処理についても、std140 レイアウトを使うかどうかでかなり実装方法が変わってきます。今回は簡単な例として、std140 を使うことを前提に、どのように uniform ブロックを初期化していけばいいのかに注目して考えてみましょう。

JavaScript 側では従来から uniform 変数のロケーションを取得するコードを記述してやる必要がありましたね。uniform ブロックを使っている場合も、考え方そのものは、これまでと変わりません。ただし、利用するメソッドはそれ専用のものになります。

まずは確認の意味で、再度今回のサンプルで使っているシェーダのコードを掲載してみます。

uniform ブロックを含むシェーダのコード

// 頂点シェーダ
#version 300 es
layout (location = 0) in vec3 position;

layout (std140) uniform matrix {
    mat4 mvp;
} mat;

uniform float scale;

void main(){
    gl_Position = mat.mvp * vec4(position * scale, 1.0);
}

// フラグメントシェーダ
#version 300 es
precision highp float;

layout (std140) uniform material {
    vec4 base;
} color;

out vec4 outColor;

void main(){
    outColor = color.base;
}

さあこのコードをよく見て、どこで uniform ブロックが定義されているのか考えてみてください。

上記の頂点シェーダとフラグメントシェーダでは、合計ふたつの uniform ブロックを定義しています。ひとつめは頂点シェーダのほうにある matrix という名前のブロック、そしてふたつ目がフラグメントシェーダのほうに書かれている material です。

スムーズにイメージしやすいように補足すると、最初のブロックは行列を格納するための用途を想定して matrix という名前にしています。同様に、後者はマテリアルに関する情報を格納するブロックという想定で material という名前にしました。今回のサンプルではこのふたつの uniform ブロックがシェーダに書かれているので、それを踏まえて初期化していきます。

通常の uniform 変数のロケーションを取得する際には、これまで gl.getUniformLocation を使いましたね。uniform ブロックの場合は、ロケーションという言い方はせずに「ブロックのインデックス」というふうに言います。

uniform ブロックのインデックスを取得するためには gl.getUniformBlockIndex を使います。名前そのまんまですね(笑)

さらに、取得したブロックインデックスは、JavaScript 側(アプリケーション側)で何番として扱うのかを決めておく必要があり、この番号の割当てに gl.uniformBlockBinding を使います。

uniform ブロックのインデックスを取得する

// prg はプログラムオブジェクト
var blockIndexVS = gl.getUniformBlockIndex(prg, 'matrix');
var blockIndexFS = gl.getUniformBlockIndex(prg, 'material');
gl.uniformBlockBinding(prg, blockIndexVS, 0);
gl.uniformBlockBinding(prg, blockIndexFS, 1);

ちょっと混乱しやすいところなので、焦らず考えてみてください。

シェーダからプログラムオブジェクトを経由して uniform 変数の情報を取得するという意味では、従来からあるロケーションを取得する流れとまったく同じです。違うのは、uniform ブロックのインデックスを取得するために、別のメソッドを使っているという点だけです。

uniform ブロックを取得するための gl.getUniformBlockIndex は、第一引数にプログラムオブジェクトを、第二引数に uniform ブロックの名前を文字列で渡します。戻り値として取得した情報を、次に出てくる gl.uniformBlockBinding で整数値と結びつけることで、バッファを何番目のものとして扱うかを決めます。

このバッファを何番目として扱うか、という数値は、あとで UBO を生成した際に、それをどの uniform ブロックに対してバインドするのかに使われます。ちょっと手順としては冗長に感じるところもあるかと思いますが、uniform 変数のロケーションを取りそこに任意の番号を割当てているだけ、と考えれば難しくないですね。

UBO を生成して紐付ける

続いては Uniform Buffer Object を実際に生成していきます。

VBO や IBO を生成するときと、実は基本的な流れは何も変わりません。変わるのは、そのバッファを「どのようなバッファとして扱うか」を指定する引数の指定くらいです。

ここは簡単ですね。

Uniform Buffer Object の生成

var matrixUBO = gl.createBuffer();
var materialUBO = gl.createBuffer();

今回のサンプルでは、MVP マトリックスを渡すための UBO と、マテリアルの情報を渡すための UBO を用意しますので、上記のようにまずは空のバッファをふたつ生成しておきます。VBO を生成する場合とまったく同じ gl.createBuffer を使うだけなので簡単です。

次に、生成したバッファに対して JavaScript 側で作ったデータを割当てしていきます。ここでは、対象のバッファを UBO として扱うということを示すために、定数 gl.UNIFORM_BUFFER を用います。

定数を指定してデータを割り当てる

// mvpMatrix は 4x4 の行列
gl.bindBuffer(gl.UNIFORM_BUFFER, matrixUBO);
gl.bufferData(gl.UNIFORM_BUFFER, mvpMatrix, gl.DYNAMIC_DRAW);
gl.bindBuffer(gl.UNIFORM_BUFFER, null);

var baseColor = new Float32Array([1.0, 0.6, 0.1, 1.0]);
gl.bindBuffer(gl.UNIFORM_BUFFER, materialUBO);
gl.bufferData(gl.UNIFORM_BUFFER, baseColor, gl.DYNAMIC_DRAW);
gl.bindBuffer(gl.UNIFORM_BUFFER, null);

GLSL 側で指定したとおりのデータ型になるように注意しつつ、バッファにデータを格納するための gl.bufferData を使って処理していきます。

冒頭でも少し触れましたが、uniform 変数の多くは更新される場合が多いと思いますので、ここでは gl.bufferData の第三引数に gl.DYNAMIC_DRAW が指定されているのもポイントでしょうか。もし仮にまったく更新しない uniform 変数があるとすれば、それについては VBO などのときと同様に gl.STATIC_DRAW 等を指定しても問題ありません。

さてさて、なかなか初期化が冗長ですが、まだ終わりません。

ここまでにやってきたことを一度整理しておきましょう。

  • シェーダ側で uniform ブロックを定義
  • JavaScript 側で uniform ブロックのインデックスを取得
  • 取得したブロックインデックスに JS 側で番号を割当て
  • uniform 変数の中身となるデータを用意
  • 用意したデータを空のバッファに gl.DYNAMIC_DRAW で割当て

さあ、あと残っているのは、JavaScript 側で割当てた番号を指標にして、生成した UBO を紐付けてやる処理です。

要は、この UBO は、この番号として扱いますよ! ということを指定するわけですね。

この作業には gl.bindBufferBase メソッドを使います。

インデックス番号とデータを紐付けする

gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, matrixUBO);
gl.bindBufferBase(gl.UNIFORM_BUFFER, 1, materialUBO);

この gl.bindBufferBase は引数の意味も見ればなんとなくわかりますね。これは簡単だと思います。

こういった冗長に感じる手順も、一度独自に関数化などを行ってしまえばそれほど難しくないと思いますので、あくまでも UBO を使うために必要な手順のひとつとして、このような処理を行うということだけ理解しておきましょう。

ちょっと準備が長かったですが、これで uniform ブロックに対して JavaScript 側で生成した UBO を紐付けることができました。

VBO や IBO がそうであるように、UBO も一度バインドした状態にしておけば、レンダリングの際には自然とそれが使われるようになります。このあたりは、他のバッファと基本的には同じ振る舞い方ですね。もしも途中で UBO に格納しているデータを更新したくなったときは、UBO をゼロから再生成するのではなく gl.bufferSubData を使って更新してやればいいでしょう。

サンプル詳細とまとめ

今回用意しているサンプルでは、異なるシェーダ間で UBO を共有するという実装を行なっています。

もう少し具体的に言うと、ひとつ目のシェーダとふたつ目のシェーダで、双方に全く同じ構成の uniform ブロックが用意されています。上記で解説したのと同様の、行列を格納するための用途に使うことを想定した matrix と、マテリアルを格納するための用途を想定した material のふたつです。

ふたつのシェーダ間でまったく同じ uniform ブロックの定義がありますが、実際に中で処理するところが少しだけ違っており、マテリアルとして受け取った uniform 変数を使ってどのような着色を行うかを変えています。ひとつ目のシェーダは受け取ったままのカラーを塗りますが、ふたつ目のシェーダは受け取ったカラーを反転させてから出力するようにしてあります。

ですからサンプルを実行すると、画面にはふたつのシーンが描かれ、左右でモデルに塗られている色が変化するようになっています。全く同じ UBO をバインドした状態のまま描画しているのに、出力される色が変化していることで、UBO が共有されているということを確かめられるでしょう。

また、サンプルの中をよくよく観察してみればわかることではあるのですが、uniform ブロックを使っているからといって従来の uniform 変数の使い方が禁止されるわけではありません。両者は同時に同じシェーダ内で扱うことができるんですね。

今回のサンプルで言うと、モデルの大きさを変化させるための係数として使っている uniform 変数 scale は、今までと同じように uniform ロケーションを取得して別途プッシュする方法を使っています。このように、割と柔軟に uniform 変数の扱いを組み合わせることができるため、使い方次第で UBO がかなり便利に使える場面というのも考えられるでしょう。

一度バッファをバインドしてしまえば、複数のシェーダを切り替える際にも特に UBO のバインドし直しなどは必要ありません。uniform 変数が大量にあったり、転送しなければならない情報が多い場合などは、UBO を用いることでかなり効率的に処理を行うことができるようになります。ただし冒頭でも書きましたが、あまり規模の大きくない実装の場合は、手間を掛けてわざわざバッファを用意することにあまりメリットが無いケースもあり得ます。このあたりは、自分の実装の規模感や、シェーダ間でのデータの共有が必要なのかなどをしっかりと考慮して、適宜選択するようにしていきましょう。

ちょっと説明することが多かった上に、なかなかイメージしにくい単語やメソッドもあったと思います。落ち着いて考えていけば、単なる手順の繰り返しに過ぎないということがわかると思いますので、落ち着いて、焦らず考えてみてください。

今回も実際に動作するサンプルは以下のリンクから確認できます。

entry

PR

press Z key