光学迷彩
今回のサンプルの実行結果
フレームバッファと射影テクスチャ
前回は射影テクスチャマッピングを解説しました。射影テクスチャマッピングを用いると、プロジェクターでスクリーンに映像を投影するような形で、モデルにテクスチャをマッピングできるのでしたね。
射影テクスチャマッピング自体は、その他の様々な技術の基盤となる技術で、これをマスターしていることによってさらに多くのエフェクトを演出できます。今回はそんな射影テクスチャマッピング関連のテクニックの一つとして、光学迷彩を行なってみたいと思います。
光学迷彩というと、未来の軍事技術というイメージもありますね。要は、風景に溶け込むような特殊な外観を持つ迷彩技術の総称です。
今回のサンプルはこの光学迷彩を擬似的にシミュレートします。そもそも疑似的な擬態技術である光学迷彩をさらに擬似的にシミュレートするってのが日本語的によくわかりませんが、要はそれっぽく見えればそれでいいやってことでやってみたいと思います。
光学迷彩は先ほども書いたとおり、背景に溶け込むような外観を得ることがまず第一です。しかしここでアルファブレンドは使えません。単なるアルファブレンドでは透明人間やゴーストのような雰囲気しか演出できませんね。
光学迷彩のように見えるようにするためには、単なる透明なだけのモデルをレンダリングするのではなく、以下のような要件を満たしてやる必要があります。※これはあくまでも個人的な基準ですけども……
- 背景が少し歪んで見える
- 背景が歪むことでモデルの輪郭がうっすら見える
- モデルより奥にある別のモデルも見える状態である
- 基本的に反射光の映り込みはしない
光学迷彩では当然ですが背景が透けて見えます。しかしそれは少しだけ歪んで見えます。これにより、そこにはうっすらとモデルの輪郭が表れ、そこにモデルが存在することがわかります。また、光学迷彩を施したモデルよりも奥に存在する別のモデルも、当然透けて見える状態である必要がありますね。
そして基本的に、反射光によるハイライトは入りません。逆にハイライトが入ってしまうと、ジェル状というか、ゼリーのような見た目になってしまいます。
これらのことを踏まえて考えると、光学迷彩を実現するためには、今まで解説してきたいくつかのテクニックを組み合わせる必要があることがわかりますね。
まず、背景や、奥にあるモデルが透けて見えるということからわかるように、これにはフレームバッファを使ったオフスクリーンレンダリングが必要です。また、モデルに背景を投影するということは、オフスクリーンレンダリングされたフレームバッファのテクスチャを、モデルに射影テクスチャマッピングで投影しなければならないことがわかりますね。
そのまま射影テクスチャマッピングしただけでは、モデルが背景に完全に溶け込んでしまいますので、投影させるテクスチャの参照座標をモデルの法線を使って少しずらすという処理も必要になります。
これらのことを踏まえつつ、いよいよ実装を見ていきましょう。
三つのシェーダプログラム
まずはシェーダから解説していきます。
今回のサンプルでは頂点シェーダ内で射影テクスチャマッピング用のテクスチャ座標の算出を行なっています。これは前回のテキストで解説したとおりですのでここでは詳細な解説を割愛します。
今回のサンプルで重要なのは、メインプログラムから入ってきた係数を使って、参照するテクスチャ座標をずらす処理を行うことです。仕組みとしては、法線に係数を掛けてやり、これをもとにテクスチャ座標をずらします。
頂点シェーダ
attribute vec3 position;
attribute vec3 normal;
attribute vec4 color;
uniform mat4 mMatrix;
uniform mat4 tMatrix;
uniform mat4 mvpMatrix;
uniform float coefficient;
varying vec4 vColor;
varying vec4 vTexCoord;
void main(void){
vec3 pos = (mMatrix * vec4(position, 1.0)).xyz;
vec3 nor = normalize((mMatrix * vec4(normal, 1.0)).xyz);
vColor = color;
vTexCoord = tMatrix * vec4(pos + nor * coefficient, 1.0);
gl_Position = mvpMatrix * vec4(position, 1.0);
}
uniform 変数として入ってくる tMatrix
はテクスチャ座標変換用の行列です。そして uniform 変数 coefficient
が光学迷彩の肝である、参照するテクスチャ座標をずらす処理のために使われる係数です。
varying 変数 vTexCoord
に値を代入している部分を見てください。ここでは射影テクスチャマッピングを行なうために、テクスチャ座標変換用の行列にモデルの頂点位置を掛ける処理を行なっています。普通に射影テクスチャマッピングを行なうだけであれば頂点座標を掛けるだけでよかったのですが、今回はここに係数と法線を掛け合わせた数値を加算して、参照するテクスチャ座標をずらしています。
ちなみに今回のサンプルは、この uniform 変数 coefficient
には、HTML 内に埋め込まれた input タグからリアルタイムに値を受け取ってシェーダに送るようにしています。
さて、続いてはフラグメントシェーダです。実は、こちらは前回のフラグメントシェーダのソースとまったく同じものです。
フラグメントシェーダ
precision mediump float;
uniform sampler2D texture;
varying vec4 vColor;
varying vec4 vTexCoord;
void main(void){
vec4 smpColor = texture2DProj(texture, vTexCoord);
gl_FragColor = vColor * smpColor;
}
頂点シェーダ側でテクスチャ座標の計算まで行なっているので、フラグメントシェーダ側では特に修正する部分がないのですね。
さて、これで光学迷彩用のシェーダに関しての説明は終わりです。今回のサンプルでは上記の光学迷彩用シェーダのほかにも、普通に反射光によるライティングを行なうシェーダも使います。
これは、光学迷彩の効果をわかりやすくするために一緒にレンダリングすることになるトーラスを、普通に反射光によるライティングでレンダリングするためのシェーダです。
さらにさらに、今回のサンプルではキューブマップも使っています。これは純粋に背景をレンダリングするためです。ですからキューブマップ用のシェーダも HTML ソース内には含まれています。つまり今回のサンプルは、実に三組ものシェーダを使ってシーンをレンダリングするわけです。なんだかだいぶ壮大な感じもしますが、実際に使っている反射光ライティングシェーダやキューブマップシェーダは、過去のテキストで解説したものをほぼそのまま使っているだけです。ここでは解説しませんので、適宜以前のテキストを参照してみてください。
強敵メインプログラム
さて、いきなり不安を誘うような副題がついていますが、今回のサンプル最大の敵メインプログラムを見ていきます。
まず始めに書きますが、今回のサンプルはあくまでも今までやってきたことを応用しているだけにしか過ぎません。非常に長いソースコードですが、ポイントを絞って考えれば必ず理解できるはずです。じっくり見ていきましょう。
まず、今回用意するモデルデータは二つだけです。トーラスと、キューブです。トーラスは光学迷彩を施したものが一つ、それ以外に反射光によるライティングを施して合計 10 個レンダリングします。キューブは単純にキューブマップ用ですので、背景として一回レンダリングします。
ただし、ここで注意点としてフレームバッファを使ったオフスクリーンレンダリングを行なうことを忘れないでください。つまり、オフスクリーンレンダリングでキューブモデルを一回、トーラスを十回レンダリングします。そのあとで既定のフレームバッファにキューブを一回、トーラスを十一回レンダリングすることになります。
メインプログラムのソースコードは非常に長いですが、理解を助けるために恒常ループのコードのみ抜粋して掲載します。
恒常ループ処理のソースコード
// 恒常ループ
(function(){
// カウンタをインクリメントする
count++;
// カウンタを元にラジアンを算出
var rad = (count % 360) * Math.PI / 180;
// フレームバッファをバインド
gl.bindFramebuffer(gl.FRAMEBUFFER, fBuffer.f);
// フレームバッファを初期化
gl.clearColor(0.0, 0.7, 0.7, 1.0);
gl.clearDepth(1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// ビュー×プロジェクション座標変換行列
var eyePosition = new Array();
var camUpDirection = new Array();
q.toVecIII([0.0, 0.0, 20.0], qt, eyePosition);
q.toVecIII([0.0, 1.0, 0.0], qt, camUpDirection);
m.lookAt(eyePosition, [0, 0, 0], camUpDirection, vMatrix);
m.perspective(90, c.width / c.height, 0.1, 200, pMatrix);
m.multiply(pMatrix, vMatrix, tmpMatrix);
// キューブマップテクスチャで背景用キューブをレンダリング
gl.useProgram(cPrg);
set_attribute(cVBOList, cAttLocation, cAttStride);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cIndex);
m.identity(mMatrix);
m.scale(mMatrix, [100.0, 100.0, 100.0], mMatrix);
m.multiply(tmpMatrix, mMatrix, mvpMatrix);
gl.uniformMatrix4fv(cUniLocation[0], false, mMatrix);
gl.uniformMatrix4fv(cUniLocation[1], false, mvpMatrix);
gl.uniform3fv(cUniLocation[2], [0, 0, 0]);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_CUBE_MAP, cubeTexture);
gl.uniform1i(cUniLocation[3], 0);
gl.uniform1i(cUniLocation[4], false);
gl.drawElements(gl.TRIANGLES, cubeData.i.length, gl.UNSIGNED_SHORT, 0);
// スペキュラライティングシェーダでトーラスモデルをレンダリング
gl.useProgram(sPrg);
set_attribute(tVBOList, sAttLocation, sAttStride);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, tIndex);
for(var i = 0; i < 9; i++){
var amb = hsva(i * 40, 1, 1, 1);
m.identity(mMatrix);
m.rotate(mMatrix, i * 2 * Math.PI / 9, [0, 1, 0], mMatrix);
m.translate(mMatrix, [0.0, 0.0, 30.0], mMatrix);
m.rotate(mMatrix, rad, [1, 1, 0], mMatrix);
m.multiply(tmpMatrix, mMatrix, mvpMatrix);
m.inverse(mMatrix, invMatrix);
gl.uniformMatrix4fv(sUniLocation[0], false, mvpMatrix);
gl.uniformMatrix4fv(sUniLocation[1], false, invMatrix);
gl.uniform3fv(sUniLocation[2], lightDirection);
gl.uniform3fv(sUniLocation[3], eyePosition);
gl.uniform4fv(sUniLocation[4], amb);
gl.drawElements(gl.TRIANGLES, torusData.i.length, gl.UNSIGNED_SHORT, 0);
}
// フレームバッファのバインドを解除
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
// canvas を初期化
gl.clearColor(0.0, 0.7, 0.7, 1.0);
gl.clearDepth(1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// キューブマップテクスチャで背景用キューブをレンダリング
gl.useProgram(cPrg);
set_attribute(cVBOList, cAttLocation, cAttStride);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cIndex);
m.identity(mMatrix);
m.scale(mMatrix, [100.0, 100.0, 100.0], mMatrix);
m.multiply(tmpMatrix, mMatrix, mvpMatrix);
gl.uniformMatrix4fv(cUniLocation[0], false, mMatrix);
gl.uniformMatrix4fv(cUniLocation[1], false, mvpMatrix);
gl.uniform3fv(cUniLocation[2], [0, 0, 0]);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_CUBE_MAP, cubeTexture);
gl.uniform1i(cUniLocation[3], 0);
gl.uniform1i(cUniLocation[4], false);
gl.drawElements(gl.TRIANGLES, cubeData.i.length, gl.UNSIGNED_SHORT, 0);
// スペキュラライティングシェーダでトーラスモデルをレンダリング
gl.useProgram(sPrg);
set_attribute(tVBOList, sAttLocation, sAttStride);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, tIndex);
for(i = 0; i < 9; i++){
amb = hsva(i * 40, 1, 1, 1);
m.identity(mMatrix);
m.rotate(mMatrix, i * 2 * Math.PI / 9, [0, 1, 0], mMatrix);
m.translate(mMatrix, [0.0, 0.0, 30.0], mMatrix);
m.rotate(mMatrix, rad, [1, 1, 0], mMatrix);
m.multiply(tmpMatrix, mMatrix, mvpMatrix);
m.inverse(mMatrix, invMatrix);
gl.uniformMatrix4fv(sUniLocation[0], false, mvpMatrix);
gl.uniformMatrix4fv(sUniLocation[1], false, invMatrix);
gl.uniform3fv(sUniLocation[2], lightDirection);
gl.uniform3fv(sUniLocation[3], eyePosition);
gl.uniform4fv(sUniLocation[4], amb);
gl.drawElements(gl.TRIANGLES, torusData.i.length, gl.UNSIGNED_SHORT, 0);
}
// テクスチャ変換用行列
m.identity(tMatrix);
tMatrix[0] = 0.5; tMatrix[1] = 0.0; tMatrix[2] = 0.0; tMatrix[3] = 0.0;
tMatrix[4] = 0.0; tMatrix[5] = 0.5; tMatrix[6] = 0.0; tMatrix[7] = 0.0;
tMatrix[8] = 0.0; tMatrix[9] = 0.0; tMatrix[10] = 1.0; tMatrix[11] = 0.0;
tMatrix[12] = 0.5; tMatrix[13] = 0.5; tMatrix[14] = 0.0; tMatrix[15] = 1.0;
// 行列を掛け合わせる
m.multiply(tMatrix, pMatrix, tvpMatrix);
m.multiply(tvpMatrix, vMatrix, tMatrix);
// 光学迷彩に掛ける係数
var coefficient = (eRange.value - 50) / 50.0;
// フレームバッファテクスチャをバインド
gl.bindTexture(gl.TEXTURE_2D, fBuffer.t);
// 光学迷彩でトーラスモデルをレンダリング
gl.useProgram(dPrg);
set_attribute(tVBOList, dAttLocation, dAttStride);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, tIndex);
m.identity(mMatrix);
m.rotate(mMatrix, rad, [1, 0, 1], mMatrix);
m.multiply(tmpMatrix, mMatrix, mvpMatrix);
gl.uniformMatrix4fv(dUniLocation[0], false, mMatrix);
gl.uniformMatrix4fv(dUniLocation[1], false, tMatrix);
gl.uniformMatrix4fv(dUniLocation[2], false, mvpMatrix);
gl.uniform1f(dUniLocation[3], coefficient);
gl.uniform1i(dUniLocation[4], 0);
gl.drawElements(gl.TRIANGLES, torusData.i.length, gl.UNSIGNED_SHORT, 0);
// コンテキストの再描画
gl.flush();
// ループのために再帰呼び出し
setTimeout(arguments.callee, 1000 / 30);
})();
恒常ループ内部では useProgram
メソッドを使って利用するシェーダを逐一切り替えながら処理しています。まず冒頭では、フレームバッファをバインドしてオフスクリーンレンダリングを行ないます。
ここでは、背景用のキューブ、反射光ライティングトーラス 10 個をレンダリングします。レンダリングした結果はフレームバッファにアタッチしたテクスチャにレンダリングされますので、ここでレンダリングされた風景が最終的に光学迷彩に使われる背景になります。
フレームバッファへのレンダリングが終わったらバインドを解除し、既定のフレームバッファに最終的なシーンのレンダリングを開始します。ここでも、背景用のキューブ、さらにライティングされた 10 個のトーラスをレンダリングします。
ここまでの処理が終わると、あとは光学迷彩用のトーラスを一つ描画するだけになりますね。
前回のテキストで解説した射影テクスチャマッピングを行なうために、テクスチャ座標変換用の行列を生成し、さらに HTML から光学迷彩の歪み係数を取得します。その部分だけを抜粋したのが以下のコード。
// テクスチャ変換用行列
m.identity(tMatrix);
tMatrix[0] = 0.5; tMatrix[1] = 0.0; tMatrix[2] = 0.0; tMatrix[3] = 0.0;
tMatrix[4] = 0.0; tMatrix[5] = 0.5; tMatrix[6] = 0.0; tMatrix[7] = 0.0;
tMatrix[8] = 0.0; tMatrix[9] = 0.0; tMatrix[10] = 1.0; tMatrix[11] = 0.0;
tMatrix[12] = 0.5; tMatrix[13] = 0.5; tMatrix[14] = 0.0; tMatrix[15] = 1.0;
// 行列を掛け合わせる
m.multiply(tMatrix, pMatrix, tvpMatrix);
m.multiply(tvpMatrix, vMatrix, tMatrix);
// 光学迷彩に掛ける係数
var coefficient = (eRange.value - 50) / 50.0;
前回の射影テクスチャマッピングでは、視点、つまりカメラの位置とプロジェクターの役割を果たすライトの位置が一致していませんでした。そこで、ライトから見た場合のビュー・プロジェクション行列をそれぞれ用意しましたね。
今回の場合は、投影するテクスチャはカメラの座標から投影される状態(つまりプロジェクターのファインダーから世界を眺めている状態)ですので、テクスチャ座標変換用の行列を生成する際に、視点のビュー・プロジェクション行列をそのまま使います。
また、上記の抜粋コード内で tMatrix
の各要素に値を代入している部分で、インデックス 5 の部分に 0.5 という値を設定していますね。
前回のテキストでは、ここは -0.5 になっていました。今回のサンプルと前回のサンプルで、どうしてこの部分の値が正負反転しているのかわかるでしょうか。
WebGL では、いろんな場面で画像座標系とテクスチャ座標系の違いに注意しなければならない場面が出てきます。これは、テクスチャ座標系では原点が左下であることから、上下が反転してしまうという現象が起こるからですね。
しかし、フレームバッファに描き込まれた風景は、最初からテクスチャへレンダリングされるわけですから、画像座標系と比較すると始めから既に上下が反転した状態になっています。前回の射影テクスチャマッピングでは、画像を読み込んでテクスチャとして利用していたためテクスチャ変換行列を使って上下を反転するような処理を行なっていました。しかし今回の場合はそもそも最初から上下反転した状態でレンダリングされているものを利用するわけですから、あらためて反転処理を入れる必要性はないわけです。
若干ややこしいですが、落ち着いて考えてみてください。
尚、HTML 内の input タグから入ってくる値は 0 ~ 100 の範囲を取ります。つまり、上記のようなコードで処理した場合、シェーダに送られる係数の範囲は -1 ~ 1 の範囲に収まることになります。
まとめ
さて、光学迷彩の実装、いかがでしたでしょうか。
コードだけを見ると非常に大量なので、少し気後れしてしまうかもしれません。ただ、やっていることは今までのテキストで解説してきたことの応用だけです。なかにはそのまま流用しているだけの技術もあります。
いくつかの技術の合わせ技によって、一つの印象的なエフェクトが生まれるということはよくあることです。今回の光学迷彩も、そういったものの一つですね。
今回のサンプルでは光学迷彩に適用させる歪みの係数を、HTML 側で自由に調節できるようになっています。実際にサンプルを動作させながら、どのような変化が起こるのか確かめてみてください。尚、これは余談ですがサンプルでは最初、歪み係数が 0 の状態になっています。つまり、モデルが完全に背景に溶け込んでほとんど目視できない状態になっていますのでご注意ください。