VBOを逐次更新しながら描画する
今回のサンプルの実行結果
ダイナミックな頂点データの更新
前回は動画ファイルのクロマキー合成を扱いました。リアルタイムにその場で動画ファイルを加工しながら、動的に描画結果に動画を合成するこの方法は、工夫次第で様々な用途があると思います。サンプル自体はシンプルなもので、あまりそのまま利用できるような内容ではなかったと思いますが、応用することでかなり表現の幅を広げることができる技術だったのではないでしょうか。
さて、今回は前回までの動画ファイルや WebRTC を使ったものとはだいぶ趣向の違うテーマになります。
今回のテーマはダイナミックな VBO の更新です。と、言葉で表現してもなんのことやらという感じもしますね。どういうことかと言うと、従来の当サイトのサンプルでは、原則として VBO は最初に javascript の配列を用いて頂点データの定義を行い、あとは行列やシェーダによって加工を行うスタイルで一貫して行っていました。ダイナミックな VBO の更新とは、この「一度生成した VBO をずっと利用し続ける」方法とは根本的に違い、動的に「VBO の中身を更新し続ける」というやり方です。
たとえば、球体を形作る頂点座標のデータがあったとします。従来のやり方では、たとえばモデル座標変換行列を使うなどして球体を移動させたり、拡大縮小させたり、あるいは回転させたりといったことを行っていました。球体を形作る頂点データは最初に生成したところから一切触れることはなく、単に VBO のバインドだけを行っていました。
ダイナミックな VBO の更新では、一度生成した VBO を動的に更新し続けるという処理を行うことができます。行列やシェーダによる頂点位置の加工を行うのではなく、javascript 側で任意に頂点のデータを操作することができるのですね。
このような方法にどんなメリットがあるのかを考えてみると、実のところ、あまり強力なメリットはないのかもしれません。というのは、シェーダを利用する場合は GPU の強力な演算能力を利用することができます。一方で、javascript で頂点データの更新を行うとなると、そこは CPU に負担してもらうことになります。少なくとも、演算能力では GPU を利用できるシェーダによる運用のほうが優れています。
しかし、たとえば一度 javascript で組んだロジックをそのまま頂点の位置データに反映させたいケースや、頂点色の動的な書き換えを行いたいケースなど、より javascript に近い部分で頂点を操作したい場合には有効な手段のひとつとなり得ます。
というわけで、極めて高い実用性があるわけではありませんが、こういった実装方法もあるのだという一例として、今回は進めていければと思います。
VBO の生成をおさらい
WebGL はあらゆる場面で非常に冗長な手順を必要とする都合上、大抵の場合、自作かあるいは慣れ親しんだライブラリを使っているケースが多いと思います。そこで、まずは従来の VBO の生成手順からおさらいしてみることにしましょう。
VBO は、単なる javascript の配列を元データとして生成します。たとえば頂点座標の VBO を生成する場合には次のような手順で処理を行います。
VBO の生成手順
// 頂点データから VBO を生成
var vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(頂点データの配列), gl.STATIC_DRAW);
上記のようにすると、変数 vertexBuffer
に VBO が格納された状態となり、さらに javascript で定義した頂点データが、その VBO に適用されます。
この VBO を利用したい場合には、さらに続けて描画までのどこかのタイミングで、以下のような処理を行う必要がありました。
描画までに必要となる手順
var attLocation = gl.getAttribLocation(programs, 'position');
gl.enableVertexAttribArray(attLocation);
gl.vertexAttribPointer(attLocation, 3, gl.FLOAT, false, 0, 0);
シェーダ側で定義されているアトリビュートのロケーション情報を取得し、そのロケーションを指定して有効化することによって、その時点でバインドされている VBO のデータがアタッチされるのでしたね。
さて、ここまでが従来の VBO の生成から利用するところまでの手順でした。
このような従来の手順では、最初に VBO にデータを格納した瞬間から VBO の中身は一切変化しません。頂点を動的に操作したい場合には、これまでシェーダなどを活用するしか方法がありませんでした。今回のテーマである動的な VBO 更新の方法を利用すると、javascript から動的に VBO の中身を操作することができるようになります。VBO に格納することでブラックボックス化してしまう頂点のデータを、javascript で動的に操作できるようになるわけですね。
では具体的にどのようにそれを実現するのか。次項よりさらに詳しく見ていきましょう。
TypedArray を活用する
動的に VBO の中身を書き換える、とは言っても、実は VBO を直接メモリ上に展開してどうこう……といったことを行うわけではありません。ポイントとなるのは、javascript の TypedArray の扱い方です。
先ほどの VBO の初期化時のコードを再度引用します。
VBO の生成手順
// 頂点データから VBO を生成
var vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(頂点データの配列), gl.STATIC_DRAW);
これをよく見るとわかるように、頂点の元データ(javascript の配列)は、VBO に適用される際に TypedArray に変換されているのがわかりますね。TypedArray は、もう少し詳しく言うと javascript の型付き配列と呼ばれるもので、メモリの管理が通常の配列に比べ厳密であり WebGL の実装には欠かせないもののひとつです。
上記のコードでも登場している gl.bufferData
メソッドが、VBO に TypedArray のデータを適用してくれる役割を持っています。そして、このメソッドの最後の引数に指定されている gl.STATIC_DRAW
に今回は注目してみましょう。
この gl.bufferData
の第三引数には、今まで当サイトのテキストでは一度も言及ことはありませんでした。この第三引数の役割は、VBO の元となるデータをどのようなアクセス頻度で扱うかを指定するものです。従来から利用していた gl.STATIC_DRAW
の場合、「データは基本的に更新されないもの」として扱われます。少し違う言い方をすると、更新されるのは一度切りである、というような指定になります。
この第三引数にはその他にも gl.STREAM_DRAW
や gl.DYNAMIC_DRAW
が指定できます。この二種類の違いはちょっと曖昧ですが、STREAM の方は数回程度、そして DYNAMIC のほうは頻繁に、頂点データが更新されるものとして解釈されます。つまり、アニメーションするような、常にループを回すような処理に組み込む場合には gl.DYNAMIC_DRAW
を用いればいいということになります。
このように gl.bufferData
メソッドの第三引数に与える指定を変更することにより、VBO のデータをどういった頻度で更新するのかを WebGL 側に通知することができるわけですね。
ダイナミックな更新のための手順
さて、それでは実際に動的な VBO の更新を行うための手順を見ていきます。
考え方のポイントとなるのは、あくまでも VBO 自体はブラックボックスであり、動的に更新できるのは javascript で扱うことができる配列のデータである、というところ。ここを勘違いしてしまうとややこしくなります。
まずは、javascript の通常の配列を用いるか、あるいは直接でも構いませんが、頂点データを格納した状態の Float32Array
の配列を用意します。TypedArray には、javascript の配列が持っている push
や concat
といった補助的なメソッドは備わっていません。しかし要素を参照する方法自体は通常の配列と変わりません。
今回のサンプルでは、minMatrix.js に含まれている球体メッシュを生成する関数のデータから、型付き配列のデータを作っています。
型付き配列のデータを用意する
// VBO生成
var pointSphere = sphere(64, 64, 1.0);
var pointPosition = new Float32Array(pointSphere.p);
上記のようにすると、変数 pointPosition
には、型付き配列の頂点座標データが格納された状態になります。
この型付き配列のデータを逐次更新しながら、ループ処理のなかで動的に VBO に割り当ててやることにより、VBO が動的に更新されるようになります。もちろん、データを割り当てる際の gl.bufferData
メソッドの第三引数には gl.DYNAMIC_DRAW
を指定してやります。
レンダリングのための恒常ループの中で、どのように処理しているのかも見てみましょう。
ループ中の処理
// 点を更新
for(var i = 0, j = pointPosition.length; i < j; i += 3){
var t = Math.cos(rad); // rad = ラジアン
var x = pointSphere.p[i] + pointSphere.p[i] * t;
var y = pointSphere.p[i + 1] + pointSphere.p[i + 1] * t;
var z = pointSphere.p[i + 2] + pointSphere.p[i + 2] * t;
pointPosition[i] = x;
pointPosition[i + 1] = y;
pointPosition[i + 2] = z;
}
gl.bufferSubData(gl.ARRAY_BUFFER, 0, pointPosition);
ここでは型付き配列である pointPosition
の中身を直接書き換えたあと、最終的に gl.bufferSubData
を呼び出して VBO にデータを反映させているのがわかりますね。一見して文字数が多く見えると思いますが、コサインの値を動的に加算しているだけですので難しいことはなにもやっていません。
bufferData と bufferSubData
2016 年 5 月の追記です。
これまで、この部分では gl.bufferData
を使ったバッファの更新をするように書いていましたが、@otnama さんにご指摘をいただき、 gl.bufferSubData
を利用するように修正しました。
両者は名前がとても似ていますが、その実態は全く異なっており、 gl.bufferSubData
のほうを利用すれば GPU 上のデータだけを更新することができ、動的な VBO の更新として正しく機能します。一方で従来掲載していた gl.bufferData
のほうを使ってしまうと、メモリの割り当てから全てやり直すことになり、非常にオーバーヘッドの大きな処理になってしまうのだそうです。
もちろん gl.bufferData
を使っていても動作自体は可能な場合が多いかと思いますが、そもそも使い方が間違っているということだったんですね。反省しました。ご指摘をいただけて、ひとつかしこくなりました。otnama さん、ありがとうございます。
なお gl.bufferSubData
の第二引数には、バイト単位でのオフセット量が整数で指定できます。全体ではなく、一部分だけを更新するなんて使い方もできるのですね。
今回のサンプルでは、モデル座標変換行列で拡大縮小や回転などは一切行っていません。シェーダ側で行っているのはビュー行列やプロジェクション行列による座標変換のみです。しかし、VBO の中身を動的に書き換えているため、行列で座標変換を行っていないにもかかわらず頂点の位置が動的に変化しているのが確認できるはずです。
javascript で行った計算の結果を、リアルタイムに頂点座標に反映させることができたわけですね。
まとめ
さて、VBO を動的に更新する方法について見てきましたがいかがでしたでしょうか。
正直なところ、あまり実用性がある内容ではないと思うのですが、なにかしらの需要がありそうな気もしたのと、自分でやったことがなかったので挑戦してみました。
パーティクルのように、たくさんの頂点をぐりぐりと動かすようなタイプのデモでは、いかに高速に効率よく大量の頂点を動かすのかがポイントになりますね。しかし、こういった場合にシェーダのみでガチパーティクルデモを作るとなると、かなり難易度が高くなってしまいます。一方で、今回のように javascript で計算した結果を、動的に VBO に割り当てることができるのであれば、慣れ親しんだ javascript でロジックを組み、それを利用してパーティクルを動かすといったことが可能になります。
計算の速度で言えば GPU で処理できるシェーダが一枚も二枚も上手です。しかし、実装のしやすさという点で、今回のテクニックは利用する価値があるのではないかなと思います。
用意したサンプルでは、頂点を gl.POINTS
で点描画しています。点のサイズは、HTML に埋め込まれた input 要素を使って動的に変更できるようになっています。実際に動作する様子を確認してみてください。