VBO逐次更新でパーティクル描画
今回のサンプルの実行結果
行列なんて要らないぜ!
前回は VBO をダイナミックに更新しながら描画することで、javascript によって加えられた座標の変更が、ダイレクトに頂点に反映される処理を実装しました。
従来は、VBO は一度配列から生成した時点からその中身を触ることはなく、頂点の操作は行列やシェーダに完全に任せる形で処理していました。しかし、直接 VBO にあてがうデータを javascript で逐次更新することで、より柔軟な処理が行えるのでしたね。
今回は、前回の内容を少し改造して大量のパーティクルを描くことに挑戦してみましょう。しかも、行列は使いません。WebGL なのに行列なしでパーティクル描画が行えるの? と思う方もいらっしゃるかもしれませんが、そこは普通にできますので安心してください。
今回の方法は前回同様に javascript 側でパーティクルの座標を動的に計算してやり、それを VBO に割り当ててやります。つまり、座標の計算はすべてjavascript で行うことになる点に注意しましょう。単純計算に非常に強い GPU ではなく、CPU が座標計算のために力を振り絞ることになります。
なぜ行列が不要なのか
実装方法について解説する前に、まずどうして行列を使わずに描画が行えるのかを考えてみましょう。
3D プログラミングにある程度熟練していれば、行列を用いずとも描画が行えることに、なんの不思議もないでしょう。ただ、これまでずっと行列を使って描画を行ってきたので、行列を一切使わずに描画を行うということが具体的にどういうことなのか、今ひとつイメージ出来ない人もいるかもしれません。
まず大前提として、頂点シェーダには出力が必須の組み込み変数がありましたね。 gl_Position
です。この変数には、モデル・ビュー・プロジェクションの各座標変換を適用した頂点の情報を代入するような処理を記述することが多いです。ただしここで大事なのは gl_Position
に入れる値に「行列変換を行わなければならないなんていう決まりはそもそも無い」ということです。
行列を使うのは、あくまでもそれが頂点の座標を加工するのに便利だからであって、WebGL の仕組み上、行列による処理が必須ということではありません。直接 gl_Position
に値を出力したからと言って、それでエラーになったりはしません。
当たり前といえば、当たり前のことですね。
直接 gl_Position
に値を出力する場合は、その値が正規化デバイス空間上に直接置かれるような状態になります。つまり、XYZ の全ての値が -1.0 から 1.0 の範囲に収まっている頂点だけが描画されます。話をわかりやすくするために正方形の canvas で考えると、左の端では X が -1.0 になり、右端で X が 1.0 になるわけですね。Y であれば、上の端が 1.0 で下の端が -1.0 です。
今回のパーティクルでもは、Z 軸については無視して X と Y だけで考える仕様です。ですから、行列をわざわざ使わなくても普通に狙ったところに頂点を配置できます。ただしこれが三次元でパーティクルを動かすということになると、やはりカメラやパースの問題が出てくるので行列を使ったほうがいいでしょう。簡単に要約すると、二次元で XY についてだけ考える場合には、無理に行列を使わなくてもいいわけです。頂点の座標を -1.0 から 1.0 の範囲に正規化して配置してやれば、普通に gl_Position
にそのまま代入するだけで頂点は狙った位置にしっかり描画されるはずです。
シェーダはとても簡素
今回はシェーダのソースコードから見ていきましょう。
それほどコードの文章量も多くないですね。
シェーダのソースコード
// 頂点シェーダ
attribute vec2 position;
uniform float pointSize;
varying vec4 vColor;
void main(void){
gl_Position = vec4(position, 0.0, 1.0);
gl_PointSize = pointSize;
}
// フラグメントシェーダ
precision mediump float;
uniform vec4 pointColor;
void main(void){
gl_FragColor = pointColor;
}
今回はパーティクルを二次元で考えるので、attribute 変数 position
は vec2
になっている点に気をつけましょう。頂点シェーダではパーティクルを点で描くためのポイントサイズの指定も行っています。
フラグメントシェーダでは、uniform 変数として入ってきた頂点の色を使ってそのまま着色するだけです。どちらのシェーダもとても簡素ですね。特に難しいところはないと思います。
javascript で頂点座標を更新
今回のパーティクルは、すべて頂点を点として描画します。パーティクルの初期位置は、正方形の canvas 全体に敷き詰められるような感じになるよう、調整してあります。
パーティクルは絶えず動いているのではなく、頂点ごとに進行方向ベクトルを持たせ、さらに速度を管理することによって動かすようにします。
頂点の初期化
// VBO生成
var position = []; // 頂点座標
var vector = []; // 頂点の進行方向ベクトル
var resolutionX = 100; // 頂点の配置解像度X
var resolutionY = 100; // 頂点の配置解像度Y
var intervalX = 1.0 / resolutionX; // 頂点間の間隔X
var intervalY = 1.0 / resolutionY; // 頂点間の間隔Y
var verticesCount = resolutionX * resolutionY; // 頂点の個数
(function(){
var i, j, x, y;
for(i = 0; i < resolutionX; i++){
for(j = 0; j < resolutionY; j++){
// 頂点の座標
x = i * intervalX * 2.0 - 1.0;
y = j * intervalY * 2.0 - 1.0;
position.push(x, y);
// 頂点のベクトル
vector.push(0.0, 0.0);
}
}
})();
上記のコードをよく見るとわかると思いますが、頂点の個数はひとまず 100 * 100 で 10,000 個です。javascript で座標を更新し続けることになるので、まあ安全な範囲だとこのくらいでしょう。すべて GPU で捌くのならもう二桁くらい個数を増やしても行ける気がします。
頂点の座標を表しているのが position
という名前の配列です。同様に vector
という配列が、頂点の進行方向を保持する役目を持ちます。
頂点の座標を更新する際には、マウスのボタンが押されているかどうかで処理が分岐します。
マウスボタンが押されているときは、マウスカーソルの位置に向かってホーミング弾のようにパーティクルが方向を変えるようにしています。それ以外の場合には速度が徐々に停止するように落ちていきます。
頂点の座標を更新する処理
// 点を更新する
for(i = 0; i < resolutionX; i++){
k = i * resolutionX;
for(j = 0; j < resolutionY; j++){
l = (k + j) * 2;
// マウスフラグを見てベクトルを更新する
if(mouseFlag){
var p = vectorUpdate(
pointPosition[l],
pointPosition[l + 1],
mousePositionX,
mousePositionY,
vector[l],
vector[l + 1]
);
vector[l] = p[0];
vector[l + 1] = p[1];
}
pointPosition[l] += vector[l] * velocity * SPEED;
pointPosition[l + 1] += vector[l + 1] * velocity * SPEED;
}
}
gl.bufferSubData(gl.ARRAY_BUFFER, 0, pointPosition);
ここで唐突に怪しげな関数が出てきているのがわかるでしょうか。
上記のコードで言うと中段のあたりに vectorUpdate
という名前の関数が呼び出されている箇所があります。この関数は、現在の頂点の座標、マウスカーソルの位置、現在の頂点の進行方向ベクトル、という情報を受け取ってホーミング弾のようなカーソルを追従する動きを再現するベクトルの計算を行ってくれる関数です。
関数の中でやっていることは、ハーフベクトルの要領でベクトル演算を行ってやり、あまり極端にカーブしすぎないように補正を行っている、という感じでしょうか。
vectorUpdate 関数
// ベクトル演算
function vectorUpdate(x, y, tx, ty, vx, vy){
var px = tx - x;
var py = ty - y;
var r = Math.sqrt(px * px + py * py) * 5.0;
if(r !== 0.0){
px /= r;
py /= r;
}
px += vx;
py += vy;
r = Math.sqrt(px * px + py * py);
if(r !== 0.0){
px /= r;
py /= r;
}
return [px, py];
}
今現在の頂点の座標と、マウスカーソルの座標、両者を減算処理してから正規化します。その結果が格納されるのが変数名で言うと r
という変数になります。ここで、極端にカーブしすぎて追従する動きがきつくなり過ぎないように 5.0 を掛けて補正しています。
あとは、ハーフベクトルを計算して再度正規化し、それを返却している感じですね。この返却されたベクトルを使って頂点を動かすわけですね。
ここで計算されているのはあくまでも進行方向ベクトルなので、頂点がどれくらい動くのかは変数 velocity
の値によって変化します。
ここでは断片的なコードしか掲載していませんが、実際にサンプルのソースコードを全体を通して見てみると、処理の流れがわかりやすいと思います。基本的なベクトル演算や、マウスに関連するイベント処理などが理解できれば、それほど難しいことをやっている箇所はないので、ぜひ頑張って読みといてみてください。
まとめ
前回の内容と併せて見てみると、javascript で直接頂点の座標を動かす処理について、かなり理解が深まると思います。
この方法の弱点はやはり、何を差し置いても CPU で処理すること、これに尽きます。WebGL は GPU を利用することで非常に高速に処理を行うことができる技術です。しかし前回と今回で紹介した方法では、頂点の座標計算を CPU 側でやることになります。これは大きなデメリットだと言えるでしょう。しかしその一方では、javascript で記述できることで見通しのよいプログラムが書けます。GPU を全力で活用するには、GLSL の多少難解な記述も必要になりますが、今回のような実装方法であれば、シェーダのソースは非常に簡素なもので済んでしまいます。
もし手っ取り早く書き慣れた javascript でパーティクルを実装してみたい、という状況であれば、今回のサンプルのような実装方法を検討してみてもいいのかもしれません。ポイントスプライトと組み合わせれば、そこそこ見れるものが簡単に作れるでしょう。
今回のサンプルは以下のリンクから参照できます。ぜひ参考にしてみてください。