ポイントスプライト
今回のサンプルの実行結果
点のレンダリングを発展させる
前回は WebGL を使って点や線をレンダリングする方法を解説しました。
線のレンダリングではプリミティブタイプの指定を行なうことで、同じ数の頂点をレンダリングしたとしても描画結果が変化しました。点のレンダリングにおいては、シェーダ側で適切な処理を行うことで、ポイントサイズを任意に変更することも可能なのでしたね。
今回は、点の大きさを変更することによって実現可能となるポイントスプライトについて解説します。以前のテキストでビルボードを解説したときにも少し触れましたが、ポイントスプライトを用いればビルボード処理と同じようなことが実現できます。しかも、通常のビルボードの実装に比べ、ポイントスプライトならではのメリットもあります。
ビルボードは、常にカメラの方向に対して垂直な姿勢を維持するように、適切な行列演算を行なう必要がありました。しかしポイントスプライトにはそういった処理は必要ありません。その分、計算の負荷が少なくて済みます。しかもその実装は、ほとんどが点をレンダリングすることと同じです。前回のテキストの内容を十分に理解しているなら、ポイントスプライトを実装するのは非常に簡単なはずです。
ただし、ポイントスプライトも単にいいことづくめというわけではありません。
前回のテキストでも書いたように、WebGL においてレンダリングできる点の大きさはハードウェア依存です。このテキストを執筆している時点では、多くの環境でポイントサイズの最大値が 64 ピクセル以下だと考えたほうが無難でしょう。
ということは、それ以上に大きなサイズのビルボードを実装しようとすれば、必然的にポイントスプライトは使えないことになってしまいます。このあたりは、実装したい処理の内容や、求める実行環境のスペックなどとのトレードオフになってしまうでしょう。
ただ、計算の負荷が小さく、比較的簡単に実装できるポイントスプライトは非常に有用な演出手段の一つです。是非、習得していただければと思います。
そもそもポイントスプライトとは
そもそもどうしてポイントスプライトを使うとビルボードと同じような処理が実現できるのでしょうか。
これは、前回のサンプルをよく観察するとわかります。たとえば、次の画像をよく見てください。
参考画像
これは前回のサンプルでポイントサイズを 10 ピクセルにしたうえでキャプチャし、それを拡大した画像です。
これを見ると、レンダリングされた全ての点は必ず同じ大きさの正方形として描画されているのがわかりますね。点のレンダリングでは、あくまでも描画されるのは[ 点 ]にしか過ぎないので、点の大きさが変化したとしてもそれらにパースが掛かったり、三次元空間上での向きが変わったりはしないのです。
言い換えれば、WebGL における点のレンダリングでは、全ての点はあたかもビルボードのように、カメラに対して常に垂直な姿勢でレンダリングされるのです。そもそも点なので、姿勢もくそもないわけですが……
これが、ビルボードの代用としてポイントスプライトが使えるということの意味です。こうしてレンダリングされた点の一つ一つに、もしテクスチャが適用できたとしたらそれはまさにビルボードと同様の処理に使えますね。
さて、それでは実装方法を見ていきましょう。
ポイントスプライトを実装するシェーダ
今回は、ポイントスプライトとして表示させるテクスチャに以下のものを使います。
ポイントスプライト用テクスチャ
背景が白いのでわかりにくいかもしれませんが、この画像はロゴの背景部分が透明の透過 PNG です。
さて、この画像を、レンダリングされる点にテクスチャとして割り当てることができれば、ポイントスプライトを使ったビルボード処理が実現できます。
基本は、前回のサンプルと同じです。要は、レンダリングするのは点にしか過ぎません。今回のサンプルでプラスするのは、テクスチャ関連の処理と、シェーダへの修正くらいのものです。まずは、シェーダを含む HTML のソースから見ていきましょう。
サンプルの HTML ソース
<html>
<head>
<title>wgld.org WebGL sample</title>
<script src="minMatrixb.js" type="text/javascript"></script>
<script src="script.js" type="text/javascript"></script>
<script id="vs" type="x-shader/x-vertex">
attribute vec3 position;
attribute vec4 color;
uniform mat4 mvpMatrix;
uniform float pointSize;
varying vec4 vColor;
void main(void){
vColor = color;
gl_Position = mvpMatrix * vec4(position, 1.0);
gl_PointSize = pointSize;
}
</script>
<script id="fs" type="x-shader/x-fragment">
precision mediump float;
uniform sampler2D texture;
uniform int useTexture;
varying vec4 vColor;
void main(void){
vec4 smpColor = vec4(1.0);
if(bool(useTexture)){
smpColor = texture2D(texture, gl_PointCoord);
}
if(smpColor.a == 0.0){
discard;
}else{
gl_FragColor = vColor * smpColor;
}
}
</script>
</head>
<body>
<canvas id="canvas"></canvas>
<p>
<input id="point_size" type="range" value="10" min="10" max="320"> point size (1 to 32 pixels)
</p>
<p>
<input id="lines" type="radio" name="line" checked> gl.LINES
</p>
<p>
<input id="line_strip" type="radio" name="line"> gl.LINE_STRIP
</p>
<p>
<input id="line_loop" type="radio" name="line"> gl.LINE_LOOP
</p>
</body>
</html>
今回のサンプルでも、線をレンダリングする際のプリミティブタイプと、レンダリングされる点のポイントサイズを変更できるようにしています。また、前回のサンプルと比較して頂点シェーダには変更点はありません。
フラグメントシェーダ内ではちょっと特殊なことをしています。
まず uniform 変数として useTexture
というデータを受け取っています。これはレンダリングにテクスチャを利用するかどうかを表すフラグとして機能します。シェーダ内の main
関数でこのフラグを参照して処理を分岐しているのがわかると思います。
そして、テクスチャを利用するとなった場合の処理をよく見ると、見慣れない変数名が出てきていることに気が付くと思います。その部分だけを抜粋したのが以下。
gl_PointCoord
smpColor = texture2D(texture, gl_PointCoord);
ここで出てくる gl_PointCoord
という組み込み変数には、描画される点上のテクスチャ座標が入ってきます。
今までテクスチャを使った処理を行なう場合には、頂点のテクスチャ座標を texture2D
関数に渡していたと思います。しかし、レンダリングされる点そのものに対してテクスチャを適用するには、この gl_PointCoord
を使ってテクスチャ座標を設定してやります。
また、今回のサンプルで初めて登場する概念として discard
というステートメントを使っています。
この discard
というステートメントが書かれている部分に処理が差し掛かると、フラグメントシェーダは何も出力しないという挙動を選択をします。
前後のコードを見るとわかると思いますが、今回のサンプルのフラグメントシェーダでは、テクスチャに設定されているアルファ値が 0.0 だった場合のみ discard
が発動します。どうしてわざわざこのような処理を行なっているのかと言えば、これはアルファブレンドにおける問題点をカバーするためです。
たとえば、仮にこの discard
を使わずに、素直にテクスチャのアルファ値を用いたアルファブレンドを行なったとすると、実行結果は以下のようになります。
通常のアルファブレンドを使った場合
いくつかのスプライトがアルファ値の影響を受けおかしなことになっているのが見てわかりますね。
アルファブレンドでは、奥にあるものからレンダリングしていくのが基本です。ポイントスプライトを用いた処理で、点と点との距離が非常に近い場合や、カメラから見て重なってしまうような座標にある場合、適切に Z ソートなどを行なうなどしない限り希望通りの実行結果が得られない場合があるわけです。
しかしここで discard
を用いると、フラグメントシェーダは一切を出力しなくなるため、特定の場合(今回の場合テクスチャのアルファ値が 0.0 の場合)だけフラグメントを更新しないように設定できるわけですね。
javascript の修正
続いては javascript プログラムを修正します。
ここでは新たに登場したいくつかの uniform 変数へのデータの登録と、テクスチャ関連の処理を追加します。さすがにソースコード全文を掲載すると長くなるので、ポイントを絞って抜粋します。
uniform 関連
// uniformLocationを配列に取得
var uniLocation = new Array();
uniLocation[0] = gl.getUniformLocation(prg, 'mvpMatrix');
uniLocation[1] = gl.getUniformLocation(prg, 'pointSize');
uniLocation[2] = gl.getUniformLocation(prg, 'texture');
uniLocation[3] = gl.getUniformLocation(prg, 'useTexture');
まずは uniform 関連です。ここでは、テクスチャと、そのテクスチャを利用するのかどうかを表すフラグとが新しく登場しています。
また、今回はアルファ値を扱うことになるので、WebGL にブレンドが有効になることと、ブレンドファクターとを設定しておきます。
ブレンドの有効化とブレンドファクターの設定
// 各種フラグを有効化する
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
gl.enable(gl.BLEND);
// ブレンドファクター
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE);
恒常ループの中では、テクスチャのバインド処理のほか、適切な uniform 変数の登録が必要になります。ただ、ほとんどは前回のサンプルと同じ流れです。以下は、点をレンダリングする部分の周辺を抜粋したコードです。
恒常ループ内の処理を抜粋
// 点のサイズをエレメントから取得
var pointSize = ePointSize.value / 10;
// ポイントスプライトに設定するテクスチャをバインド
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
// 点を描画
set_attribute(pVBOList, attLocation, attStride);
m.identity(mMatrix);
m.rotate(mMatrix, rad, [0, 1, 0], mMatrix);
m.multiply(tmpMatrix, mMatrix, mvpMatrix);
gl.uniformMatrix4fv(uniLocation[0], false, mvpMatrix);
gl.uniform1f(uniLocation[1], pointSize);
gl.uniform1i(uniLocation[2], 0);
gl.uniform1i(uniLocation[3], true);
gl.drawArrays(gl.POINTS, 0, pointSphere.p.length / 3);
シェーダに対して適切にデータを送っているのがわかると思います。
この後の線に関する描画の部分には変更点はありません。ポイントスプライトに関する部分ではシェーダに対する修正のほうが多かった感じになりますね。
まとめ
ポイントスプライトを用いると、計算の負荷は抑えつつ、ビルボードの実装と同じように処理を行うことが可能になります。点のレンダリングを既に習得しているのなら、少々の修正を加えるだけで割と簡単に実装できてしまえると思います。
ポイントサイズの指定できる範囲に問題があるなど、必ずしも万能ではないポイントスプライトですが、使いどころをある程度選んでやれば、非常に有用なテクニックであることには変わりありません。
特に、今回の場合はアルファブレンドを普通に行ないましたが、たとえば加算合成などを用いた演出効果にポイントスプライトを活用すれば、炎や光の演出を非常に簡単に実装できるでしょう。要は、使い方次第です。
今後は、最大ポイントサイズがある程度大きな数値で安定してくれることを願うばかりです。
いつものようにサンプルへのリンクは下のほうにあります。実際に動作するサンプルを見てもらったほうが、その効果は体感しやすいと思います。
次回は、ステンシルバッファを扱います。ステンシルバッファを用いると、スクリーン上を型抜きする要領で、任意の箇所だけレンダリングしないように設定することなどが可能です。お楽しみに。