アルファブレンディング
今回のサンプルの実行結果
ブレンディング
前回はテクスチャパラメータの意味と設定方法について解説しました。今回は、テクスチャからいったん離れて、ブレンディングについて解説したいと思います。
ブレンド( blend )はご存知の通り[ 混ぜる ]という意味を持つ単語です。では、いったい何を混ぜるのかと言えば、WebGL においては色を混ぜ合わせることをブレンディングと呼んでいます。
深度テストやカリングがそうであったように、ブレンディングも既定値は無効になっています。意図的にブレンディングを有効にすることで、初めてブレンド処理を行うことができるようになるわけですね。
しかし、ブレンディングを行い、色を混ぜ合わせることによっていったいどんな処理が実現できるのでしょうか。
実は今回の主題である、アルファブレンディング――つまり、半透明描画処理を行なうために、ブレンディングが必要になります。そのほかにも、色と色を混ぜ合わせることによって生まれる様々なエフェクト処理なども、ブレンディングを用いることで比較的簡単に実現できます。
今回はブレンディングに関する基本を理解しつつ、どうしてブレンディングによって半透明処理が行なえるのか、そこまで理解してもらえればと思います。多少ややこしい内容になると思いますが、落ち着いて考えましょう。
ブレンディングとはどんな処理なのか
さて、まずはブレンディングの概念を説明します。
ブレンディングとは、先にも述べたように[ 色を混ぜ合わせる ]処理です。では、その混ぜ合わせる色とは、いったい何の色なのでしょうか。頂点の色でしょうか、それともテクスチャの色でしょうか。
答えは、頂点色でもテクスチャカラーでもありません。
描画元( これから描画されようとする色 )と、描画先( 既に描画されている色 )の二つが正解です。
思い出してみてください。WebGL では、何かをレンダリングする前に、必ずコンテキストを初期化して、色や深度値をクリアしていましたよね。連続して行なわれるループ処理であれば、恒常ループの中で clear
メソッドを実行して、コンテキストの色を毎回初期化しているはずです。
このとき、コンテキストには clearColor
メソッドで指定した色が一面に塗られることになります。これが、いわゆる描画先の色、つまり既に描画されている色ということになりますね。そこに何かをレンダリングしようとすれば、これから描画されようとしているわけですから、そのレンダリングされようとしている色が描画元の色ということになります。
仮に、既にコンテキスト上に何かがレンダリングされた後であれば、そのレンダリング後のコンテキストの色が描画先の色、これからさらにレンダリングされようとしている色が描画元の色ということになりますね。
ブレンディングを有効にするということは、この描画先と描画元、二つの色をなにかしらの方法によって混ぜ合わせられるということを意味します。二つの色をどのように混ぜ合わせるのか、その方法には様々な選択肢があります。これらの選択肢を複雑に組み合わせることによって、多彩な表現を得ることが可能です。
ブレンドファクター
ブレンドを行なう際のブレンド方法には、本当に様々な種類があります。その上、さらにそれらを組み合わせることによって初めて、最終的にコンテキスト上に描画される色が決まってきます。
正直なところ、これは非常にわかりにくい分野です。いきなり全てを理解しようとすると、はっきり言って難しい部分もあるかと思います。無理に全てを理解する必要はありませんが、まずはどんな選択肢が存在するのか、それを知っておくことはけして損にはならないはずです。
ブレンディングは先述の通り、まずは有効化しなければ利用することさえできません。まずするべきことは WebGL にブレンディングを行なうように設定することです。これには、もはやすっかりお馴染みとなった enable
メソッドを使います。
ブレンディングを有効にするコード
gl.enable(gl.BLEND);
はい、簡単ですね。組み込み定数 gl.BLEND
を引数に指定して enable
メソッドを実行することでブレンディングが有効になります。無効にする場合にも、これまで同様 disable
メソッドを使えば OK です。
ブレンディングを有効にしたら、次にブレンドファクターを設定します。※ブレンドファクター( ブレンド係数 )は一般的な呼称ではないかもしれませんが、当サイトのテキストでは、ブレンディングに関する設定を総称してブレンドファクターと呼ぶことにします。
ブレンドファクターは、描画元と描画先、それぞれに対して設定することができます。そして、それを行なうためのメソッドが blendFunc
メソッドです。このメソッドは引数を二つ取ります。第一引数には描画元に適用するブレンドファクターを、第二引数には描画先に適用するブレンドファクターを指定します。
blendFunc メソッド
gl.blendFunc(sourceFactor, destinationFactor);
上記の記述例で source とは描画ソース、つまり描画元を指します。destination は描画先ですね。では、これらに指定できるブレンドファクターを見てみます。
ブレンドファクターの一覧
定数名 | 値・式 |
---|---|
gl.ZERO | (0, 0, 0, 0) |
gl.ONE | (1, 1, 1, 1) |
gl.SRC_COLOR | (Rs, Gs, Bs, As) |
gl.DST_COLOR | (Rd, Gd, Bd, Ad) |
gl.ONE_MINUS_SRC_COLOR | (1, 1, 1, 1) - (Rs, Gs, Bs, As) |
gl.ONE_MINUS_DST_COLOR | (1, 1, 1, 1) - (Rd, Gd, Bd, Ad) |
gl.SRC_ALPHA | (As, As, As, As) |
gl.DST_ALPHA | (Ad, Ad, Ad, Ad) |
gl.ONE_MINUS_SRC_ALPHA | (1, 1, 1, 1) - (As, As, As, As) |
gl.ONE_MINUS_DST_ALPHA | (1, 1, 1, 1) - (Ad, Ad, Ad, Ad) |
gl.CONSTANT_COLOR | (Rc, Gc, Bc, Ac) |
gl.ONE_MINUS_CONSTANT_COLOR | (1, 1, 1, 1) - (Rc, Gc, Bc, Ac) |
gl.CONSTANT_ALPHA | (Ac, Ac, Ac, Ac) |
gl.ONE_MINUS_CONSTANT_ALPHA | (1, 1, 1, 1) - (Ac, Ac, Ac, Ac) |
gl.SRC_ALPHA_SATURATE | (f, f, f, 1) f = min(As, 1 - Ad) |
随分と種類がありますね。一見するとまったくもって意味がわからないかもしれませんが、気後れせずにじっくり見ていきます。
まず、上の一覧の[ 値・式 ]欄に入っている値や式は、全て四つの要素からできていますね。これは、純粋に色の各要素、つまり左から順に RGBA を表していることをまず念頭に置きましょう。そして、アルファベットの大文字で R や G などと表記されているのは、そのまま RGBA のどの要素なのかを表します。続けてアルファベットの小文字で s や d となっている部分は、それぞれ[ s = source ]・[ d = destination ]です。
最後のほうには、小文字の c も出てきますね。これは[ c = constant ]なのですが、とりあえず今はややこしくなるので気にせずいきましょう。まず重要なのは s と d を使っているブレンドファクターです。
さて、ブレンドファクターの定数名には、それがどのようなブレンディングを行なう係数なのか、ヒントとなるような名称がつけられています。たとえば、一番最初に登場している gl.ZERO
は、もうそのまんまの意味で全ての色要素が 0 になっていますね。次に出てくる gl.ONE
も、やっぱり似たような感じで全ての要素が 1 で統一されています。
WebGL でブレンドを有効にしたとき、まず既定で設定されているブレンドファクターは、次のように指定して blendFunc
メソッドを実行した場合とまったく同じになっています。
WebGL の既定値と同じ指定をした例
gl.blendFunc(gl.ONE, gl.ZERO);
さて、このように指定されていると、実際の色の計算はどうなるでしょうか。
第一引数は[ 描画元の色 ]をどう扱うのかを指定するのでしたよね。描画元の色が仮に(0.5, 0.5, 0.5, 1.0)であった場合は、これらに全て 1 が掛けられることになるので、結果は(0.5, 0.5, 0.5, 1.0)のまま、一切変化しないことになります。
一方で、第二引数に指定する[ 描画先の色 ]つまりコンテキストの色のほうはどうでしょう。第二引数には gl.ZERO
が指定されているわけですから、コンテキスト上の色がどうなっていようと、全て 0 になってしまいますね。
最終的にコンテキストに描画される色は次のような計算式によって算出されます。
描画色 = 描画元の色 * sourceFactor + 描画先の色 * destinationFactor
これを見ると結果は明白です。既定の設定では、描画元の色( source )だけが画面上に出力されることがわかりますね。描画先の色がどうなっていようと、全て描画元の色で上書きされてしまうわけですね。
このように、ブレンドファクターをいかに指定したかによって、描画元、または描画先の色をどのように扱うのかが変わってきます。これらのことを踏まえてブレンドファクターを様々に変化させると、アルファブレンドを実現することも可能になります。
アルファブレンドを実現する
それでは実際問題、どのようにブレンドファクターを指定すればアルファブレンドを行うことができるのでしょうか。
アルファブレンドはその名の通り、色のアルファ値を用いてブレンド処理を行なう処理のことです。これは言い換えると、アルファ値自体が色を操作するための特殊な係数であるということでもあります。アルファ値を参照しながら色をブレンドするので、アルファブレンドと呼ばれるわけですね。
まず、アルファブレンドを行なう前提として、コンテキスト上にもともとある色(初期化によってクリアする際の色)は、不透明な青だとここでは仮定します。不透明な青ですから、これを RGBA で表すと(0.0, 0.0, 1.0, 1.0)となりますね。
そこに、赤い色をしたポリゴンを描画しようとするとき、この赤いポリゴンを不透明度 70 %(1.0, 0.0, 0.0, 0.7)で描画するにはどうすればいいでしょうか。
青いコンテキスト上に、赤いポリゴンを不透明度 70 %で描画するわけですから、ポリゴンが描かれた場所はやや赤みがかった紫色になるはずですね。これを数値で表すと以下のようになります。
描画元(ポリゴン) + 描画先(コンテキスト) = 最終出力色
(1.0, 0.0, 0.0) * sFactor + (0.0, 0.0, 1.0) * dFactor = (0.7, 0.0, 0.3)
■ * sFactor + ■ * dFactor = ■
さて、それぞれのブレンドファクターにはどれを指定すればいいのでしょう。そもそも、アルファブレンドを行なうわけですから、アルファ値が関係しないブレンドファクターを指定しても意味がありませんね。それだけで大量にある選択肢のうちいくつかは消えるはずです。
また、描画元の色は、よく見ると色成分が 0.7 倍されていますね。描画先の色は、色成分が 0.3 倍になっています。これらのことから導き出される正解のブレンドファクターはいったいどれでしょう。
ヒントは、描画元のポリゴンのアルファ値が 0.7 であるという事実です。
答えはわかりそうでしょうか――
まぁ、いつまでも引っ張っても仕方ないので、正解を書いてしまいます。
まず、ポリゴンの色には、そのポリゴン自体が持つアルファ値をそのまま掛ければいいですね。ポリゴンの色に含まれるアルファ値は 0.7 なので、単純にそれを掛けてやればいいわけです。
描画元の色計算:
sFactor = 描画元のアルファ値(0.7) [ gl.SRC_ALPHA ]
(1.0, 0.0, 0.0) * sFactor(0.7, 0.7, 0.7) = (0.7, 0.0, 0.0)
次に描画先の色です。こちらは先ほどとは違い、B 成分の 1.0 が最終的には 0.3 になってほしいわけです。ということは、1 から描画元のアルファ値を引いた値を使えばよさそうです。
描画先の色計算:
dFactor = 1.0 - 描画元のアルファ値(0.7) = (0.3) [ gl.ONE_MINUS_SRC_ALPHA ]
(0.0, 0.0, 1.0) * dFactor(0.3, 0.3, 0.3) = (0.0, 0.0, 0.3)
さてさて、このように導き出された描画元の色と描画先の色を、先ほど示したように足し算した結果が、最終的にコンテキスト上に出力される色ということになりますね。
最終出力色の計算:
(0.7, 0.0, 0.0) + (0.0, 0.0, 0.3) = (0.7, 0.0, 0.3)
随分回りくどい感じがするかもしれませんが、これでアルファブレンドの計算がキチンと正確に成立することはわかりますね。sFactor には、描画元のアルファ値をそのまま掛けます。これはまさに gl.SRC_ALPHA
を指定した場合の処理になります。dFactor には、描画元のアルファ値を 1 から引いた値を掛ければいいわけです。これを表す定数は gl.ONE_MINUS_SRC_ALPHA
ですね。これらを総合すると、アルファブレンドを実現するためのブレンドファクターの指定は、次のようになります。
アルファブレンドを行なう指定
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
当テキストの始めのほうで載せたブレンドファクターの一覧表と見比べれば、これで正しくアルファブレンドが行なえる理由がわかるはずです。
プログラムを記述する
それではアルファブレンドを行なえるようにプログラムを記述しましょう。今回は、四角形ポリゴンを 2 枚レンダリングします。一つ目のポリゴンは、テクスチャを適用し、ブレンディングは行なわずに今までどおり描画します。二つ目のポリゴンは逆に、テクスチャは貼らずに、ブレンディングを有効にしてレンダリングします。
また今回のサンプルでは自由に透明度を変化させられるようにします。これには HTML5 から採用された新しい input タグタイプである range タイプを使います。
range タイプの例
input type range
この input タグに id を付加しておき、javascript プログラムからループのたびに参照することで、透明度を任意に変化させながらレンダリングが行なえるようにします。ちなみに、この range タイプには最小値を 0 、最大値を 100 で設定しており、これを 100 で割った数値をシェーダにプッシュします。これはアルファ値は常に 0 ~ 1 の範囲に収まっている必要があるためです。
また、今回はテクスチャを使うポリゴンと、使わないポリゴンとを同時にレンダリングしますので、シェーダに対してテクスチャを使うべきかどうかを通知する仕組みを作ります。
シェーダ内では、javascript のプログラムと同様に if
ステートメントを使うことが可能です。これを利用して、テクスチャを使う場合と使わない場合とで処理を分岐するようにします。
さあ、これらのことを踏まえて、今回のサンプルのシェーダのソースを見てみましょう。まずは、頂点シェーダからです。
サンプルの頂点シェーダ
attribute vec3 position;
attribute vec4 color;
attribute vec2 textureCoord;
uniform mat4 mvpMatrix;
uniform float vertexAlpha;
varying vec4 vColor;
varying vec2 vTextureCoord;
void main(void){
vColor = vec4(color.rgb, color.a * vertexAlpha);
vTextureCoord = textureCoord;
gl_Position = mvpMatrix * vec4(position, 1.0);
}
今回は attribute
変数として頂点の[ 位置 ]・[ 色 ]・[ テクスチャ座標 ]の三つの情報を受け取ります。これは簡単ですね。さらに uniform
変数として[ 座標変換行列 ]・[ input range タイプから取得した透明度 ]の二つを受け取ります。
フラグメントシェーダに送る varying
変素も二つありますね。色情報とテクスチャ座標の情報を送るようにしています。ここで注目すべきは変数 vColor
に値を設定している部分。
変数 vColor に値を設定している箇所を抜粋
vColor = vec4(color.rgb, color.a * vertexAlpha);
頂点から入ってきた頂点色の情報と、range タイプの input タグから取得した透明度の情報とを使って、フラグメントシェーダに送る色情報を操作しています。変数の vec4
型は、xyzw や rgba での参照が可能という柔軟な仕様になっているので、それを利用して頂点のアルファ値とタグから取得したアルファ値を掛け合わせています。
あとは、特別難しいことはしていませんね。今までやってきたことを思い出せば簡単なはずです。
続いてはフラグメントシェーダ。
サンプルのフラグメントシェーダ
precision mediump float;
uniform sampler2D texture;
uniform int useTexture;
varying vec4 vColor;
varying vec2 vTextureCoord;
void main(void){
vec4 destColor = vec4(0.0);
if(bool(useTexture)){
vec4 smpColor = texture2D(texture, vTextureCoord);
destColor = vColor * smpColor;
}else{
destColor = vColor;
}
gl_FragColor = destColor;
}
こちらは若干複雑そうに見えるかもしれませんが、やっていることはそんなに難しくありません。まず uniform
変数として二つのデータが入ってきます。一つ目( sampler2D texture
)はテクスチャに関する情報、もう一つ( int useTexture
)はテクスチャを使うのかどうかを表す係数です。
変数 useTexture
は、変数の型が int
型になっています。つまり、プログラムからは整数値としてデータが入ってきます。これをシェーダ内で真偽値を表すデータに変換して利用します。
どういうことかと言うと、フラグメントシェーダの main
関数の中を見てください。まず最初に最終的に出力される色を格納するための変数 destColor
が宣言されています。その次の行では if
ステートメントを使って何かを判別していますね。
ここで使われている bool
というビルトイン関数は、整数値などのデータを真偽値に変換してくれる関数です。変換された結果を参照して、テクスチャの色を使うのかどうか、処理を分岐させています。
整数値から真偽値にわざわざ変換するなんて、どうしてこんな回りくどいことをする必要があるのでしょうか。
これは、プログラムから uniform
変数を登録する際、直接真偽値を、真偽値として送ることができないからです。javascript プログラムのなかでシェーダに uniform
変数を登録する際には uniform4fv
や uniform1i
を使いますよね。これらのなかに uniform1b
というメソッドは存在しません。ですから直接 bool 型としてシェーダにプッシュする方法はないわけです。
これに代わる方法として uniform1i
メソッドから整数値としてデータを送り、シェーダ内で真偽値に変換するやり方を採用しているというわけです。※まぁ、いずれにしてもわざわざ真偽値に変換しなくても、整数値を比較することだけで、処理を分岐させること自体は普通にできます。これはもう、どんなふうにソースを記述したいかというプログラマ個人の趣向の問題とも言えますね。
今回は、シェーダ内で変数の型をキャスト(変換)することもできる、ということもお伝えしたかったのであえてこのように記述を行ないました。整数値を整数値のまま扱っても、別段問題があるわけではありません。
メインプログラムも見てみる
少し長いですが勢いそのまま javascript のプログラムのほうも見ていきます。
メインプログラムの中では、適切に attribute と uniform に関する処理を記述したあと、深度テストとテクスチャの利用を有効化しておきます。また、今回は単純な板ポリゴンの描画を行なうため、カリングは無効のままにしておきます。
恒常ループの中では、エレメントを参照してブレンディングのタイプを決定する処理を行ないます。これには、以下の自作関数を使います。
自作関数 blend_type
// ブレンドタイプを設定する関数
function blend_type(prm){
switch(prm){
// 透過処理
case 0:
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
break;
// 加算合成
case 1:
gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
break;
default:
break;
}
}
引数として受け取った整数値を指標に、ブレンディングの方法を変化させる関数になっています。今回のサンプルでは、通常のアルファブレンドのほかに、代表的なブレンディング手法の一つである加算合成も行なえるようにしてあります。
自作関数 blend_type
の引数に 0 を指定すれば通常のアルファブレンドが、1 を指定すれば加算合成が行なえます。アルファブレンドを行なう際のブレンドファクターの指定は先ほど解説したとおりです。加算合成のブレンドファクターは、詳細については解説しませんが blendFunc
メソッドの引数をよく見て考えれば、わかると思います。
ブレンドファクターの指定が完了したら、続いてはドキュメント内の input タグから、頂点色に掛け合わせるアルファ値を取得しておきます。
アルファ値の取得
// エレメントからアルファ成分を取得
var vertexAlpha = parseFloat(elmRange.value / 100);
ここで使われている parseFloat
関数は、引数に与えられた数値を浮動小数点数型に変換してくれる関数です。エレメントから得られる数値は 0 ~ 100 の範囲の整数値なので、この関数を使って 0 ~ 1 の範囲に正しく収まるようにしています。
続いてはポリゴンモデルを描画していきます。先述の通り、今回レンダリングするポリゴンモデルは二つです。
今回はまず、カメラから見て少し奥の座標に、テクスチャ有り・ブレンド無しでポリゴンを描画します。
一つ目のポリゴンのレンダリング
// モデル座標変換行列の生成
m.identity(mMatrix);
m.translate(mMatrix, [0.25, 0.25, -0.25], mMatrix);
m.rotate(mMatrix, rad, [0, 1, 0], mMatrix);
m.multiply(tmpMatrix, mMatrix, mvpMatrix);
// テクスチャのバインド
gl.bindTexture(gl.TEXTURE_2D, texture);
// ブレンディングを無効にする
gl.disable(gl.BLEND);
// uniform変数の登録と描画
gl.uniformMatrix4fv(uniLocation[0], false, mvpMatrix);
gl.uniform1f(uniLocation[1], 1.0);
gl.uniform1i(uniLocation[2], 0);
gl.uniform1i(uniLocation[3], true);
gl.drawElements(gl.TRIANGLES, index.length, gl.UNSIGNED_SHORT, 0);
ここでは uniform
関連の処理に注意しましょう。
uniformLocation のインデックス 0 は座標変換行列、インデックス 1 が頂点のアルファ値にシェーダ内で掛け合わせるアルファ値です。インデックス 2 はテクスチャユニットの登録のために使い、インデックス 3 がテクスチャを使うかどうかを表すデータです。
続いて、テクスチャ無し・ブレンド有りで描画される二つ目のポリゴンの処理を見てみます。
二つ目のポリゴンのレンダリング
// モデル座標変換行列の生成
m.identity(mMatrix);
m.translate(mMatrix, [-0.25, -0.25, 0.25], mMatrix);
m.rotate(mMatrix, rad, [0, 0, 1], mMatrix);
m.multiply(tmpMatrix, mMatrix, mvpMatrix);
// テクスチャのバインドを解除
gl.bindTexture(gl.TEXTURE_2D, null);
// ブレンディングを有効にする
gl.enable(gl.BLEND);
// uniform変数の登録と描画
gl.uniformMatrix4fv(uniLocation[0], false, mvpMatrix);
gl.uniform1f(uniLocation[1], vertexAlpha);
gl.uniform1i(uniLocation[2], 0);
gl.uniform1i(uniLocation[3], false);
gl.drawElements(gl.TRIANGLES, index.length, gl.UNSIGNED_SHORT, 0);
一つ目のポリゴンよりも少し手前の座標にレンダリングされるように座標変換行列を作っています。また、input タグから取得した透明度に関する係数をシェーダに送っていることにも注目です。
まとめ
アルファブレンディングについて解説してきましたが、正直なところ、結構最初に理解するまでが大変なように思います。これは自分自身の経験則ですが……
しかしいったん理解できてしまえば、あとは単純な四則演算の問題です。ブレンドファクターを自由に組み合わせることでいろんな表現が行なえる原理もわかると思います。今回のサンプルでは透過処理と加算合成のみ扱いましたが、工夫次第では色を反転させたり、特殊な方法で合成したりといったことが可能です。工夫を凝らしてみると思いもよらない表現方法が見つかるかもしれません。
また、アルファブレンディングに限らず、ブレンディングに関する処理にはいろんな落とし穴があります。これらについては、次回また、詳しく解説する予定です。
サンプルへのリンクはいつも通り以下にあります。実際に動作させてみると、いろんな発見があると思います。