射影テクスチャマッピング
今回のサンプルの実行結果
スクリーン投影
前回はトゥーンレンダリングを行ないました。トゥーンレンダリングを用いるとアニメ調のレンダリングを行うことができ、シーンの雰囲気をガラリと変えることができましたね。通常のライティングの計算とテクスチャマッピングという、比較的基礎的な概念だけで実装できるトゥーンレンダリングは、見た目も非常に面白いので是非一度チャレンジしてみていただきたいですね。
さて、今回は射影テクスチャマッピングです。射影テクスチャマッピングは別名[ 投影テクスチャマッピング ]などとも呼ばれます。テクスチャをまるでスクリーンに投影するかのようにマッピングする今回のテクニックは、応用することで様々な処理に流用できます。
たとえばモデルの影を投影するのに使われたり、あるいはモデルを光学迷彩が掛かったように処理したり、利用方法はいろいろと考えられます。射影テクスチャマッピングの基本をマスターし、テクニックの底上げを行ないましょう。
射影空間とテクスチャ空間
さて、射影テクスチャマッピングはその名の通り、テクスチャという二次元のデータを射影変換することで三次元空間上に投影する処理です。テクスチャの投影を行なうわけですから、イメージとしてはプロジェクターでスクリーンに映像を投影するような感じですね。
ただしここで注意しなければならないのは、画像データの原点が左上の角位置になるのに対して、テクスチャの原点位置が左下になるという点です。このことを考慮しておかないと、最終的に投影されるイメージが上下反転してしまいます。
参考:テクスチャマッピング
また、テクスチャ空間は 0 ~ 1 の範囲で座標を表し、尚且つ原点は先述の通り左下です。ですがプロジェクション変換を行なう射影空間では、座標の範囲は -1 ~ 1 になりますし、原点は空間の中心位置になります。
イメージが上下反転してしまう問題と、テクスチャ空間と射影空間で座標系が異なる問題、この二つの問題に対処するために、テクスチャ座標系への変換行列を作成します。今回のテキストでは、この行列の導入方法までは詳細に解説しません。どのようにこの行列が導き出されるのかは、以下のサイトが参考になります。
テクスチャ座標変換行列が準備できたら、あとはライト(先ほどの話で言うところのプロジェクターですね)を視点としてビュー変換行列とプロジェクション変換行列を生成しておきます。これらの変換行列を使って、最終的に参照するテクスチャ座標を決定します。
シェーダの記述
それでは、いつものようにシェーダのソースから見ていきます。まずは頂点シェーダです。
頂点シェーダ
attribute vec3 position;
attribute vec3 normal;
attribute vec4 color;
uniform mat4 mMatrix;
uniform mat4 tMatrix;
uniform mat4 mvpMatrix;
varying vec3 vPosition;
varying vec3 vNormal;
varying vec4 vColor;
varying vec4 vTexCoord;
void main(void){
vPosition = (mMatrix * vec4(position, 1.0)).xyz;
vNormal = normal;
vColor = color;
vTexCoord = tMatrix * vec4(vPosition, 1.0);
gl_Position = mvpMatrix * vec4(position, 1.0);
}
頂点シェーダでは、頂点属性として頂点位置・頂点法線・頂点色の三つを受け取ります。また uniform 変数として三つの行列を受け取ります。上記のソースでは mMatrix
がモデル座標変換行列で tMatrix
がテクスチャ座標変換行列です。
またフラグメントシェーダに送る varying 変数として四つの変数を定義していますね。変数 vPosition
にはモデル座標変換行列を掛け合わせた頂点座標が入ります。このモデル座標変換行列を掛け合わせた頂点位置と、uniform 変数として入ってきたテクスチャ座標変換行列とを掛け合わせることで、最終的なテクスチャ座標が得られます。これを varying 変数 vTexCoord
に入れておきフラグメントシェーダへと送ります。
フラグメントシェーダでは、頂点シェーダから送られてきたテクスチャ座標 vTexCoord
を使ってテクスチャを参照します。
フラグメントシェーダ
precision mediump float;
uniform mat4 invMatrix;
uniform vec3 lightPosition;
uniform sampler2D texture;
varying vec3 vPosition;
varying vec3 vNormal;
varying vec4 vColor;
varying vec4 vTexCoord;
void main(void){
vec3 light = lightPosition - vPosition;
vec3 invLight = normalize(invMatrix * vec4(light, 0.0)).xyz;
float diffuse = clamp(dot(vNormal, invLight), 0.1, 1.0);
vec4 smpColor = texture2DProj(texture, vTexCoord);
gl_FragColor = vColor * vec4(vec3(diffuse), 1.0) * smpColor;
}
フラグメントシェーダでは、点光源による通常のライティング計算を行なう他、頂点シェーダから送られてきたテクスチャ座標を使ったテクスチャへの参照を行ないます。
今まで、二次元テクスチャを参照する際には texture2D
という GLSL の組み込み関数を使っていました。今回は頂点を射影変換した結果からテクスチャをサンプリングするため texture2DProj
という組み込み関数を使います。第一引数にはテクスチャユニット番号を指定し、これは texture2D
と同じですね。第二引数には射影変換を適用した頂点座標を渡します。
最終的に、頂点色と点光源によるライティング計算の結果、さらにサンプリングしたテクスチャカラーとを掛け合わせて色を出力します。
射影テクスチャマッピングでは、今回のように texture2DProj
を使うというところが肝になりますね。
javascript プログラムの記述
シェーダの内容を踏まえて、メインプログラムのほうも見ていきます。
メインプログラムの内部では、先述の通りテクスチャ座標変換用の行列を生成してやる必要があります。これはライト(プロジェクターの役割をするライトですね)から見た変換行列を用意する一連の流れのなかで行ないます。
また今回はこのライトの位置を調節できるようにするために、HTML に range タイプの input タグを埋め込みます。ライトの位置を原点から遠ざけると、その分投影されるテクスチャの範囲が大きくなります。これはプロジェクターを投影面から遠ざけたり近づけたりすることと意味としては同じですね。ライトが投影面に近づくほど、投影されるテクスチャの面積が狭くなります。
今回のサンプルでは、トーラスを 10 個と、テクスチャが投影されていることをわかりやすくするための壁面用の板ポリゴンをレンダリングします。ライティングは点光源でのライティングになります。
投影するテクスチャには、以下のテクスチャを使いました。
サンプルで使用するテクスチャ
さて、それではまず今回のサンプルで利用する行列の準備をしている部分を見てみます。
行列の生成と初期化
// 各種行列の生成と初期化
var m = new matIV();
var mMatrix = m.identity(m.create());
var vMatrix = m.identity(m.create());
var pMatrix = m.identity(m.create());
var tmpMatrix = m.identity(m.create());
var mvpMatrix = m.identity(m.create());
var invMatrix = m.identity(m.create());
var tMatrix = m.identity(m.create());
var tvMatrix = m.identity(m.create());
var tpMatrix = m.identity(m.create());
var tvpMatrix = m.identity(m.create());
点光源でのライティングを行なう上で、上記のうち invMatrix
までが必要ですね。これは今までのサンプルでも同様に利用してきたものです。今回のサンプル特有の行列は tMatrix
以下の四つです。
これらはライトを視点とみなした場合の変換行列として使います。実際にこれらの行列を使って処理を行なっている部分を見てみましょう。
射影テクスチャ用行列の処理
// ライトの位置
var lightPosition = [-10.0, 10.0, 10.0];
// ライトビューの上方向
var lightUpDirection = [0.577, 0.577, -0.577];
// (中略)
// テクスチャ変換用行列
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;
// ライトの距離をエレメントの値に応じて調整
var r = eRange.value / 5.0;
lightPosition[0] = -1.0 * r;
lightPosition[1] = 1.0 * r;
lightPosition[2] = 1.0 * r;
// ライトから見たビュー座標変換行列
m.lookAt(lightPosition, [0, 0, 0], lightUpDirection, tvMatrix);
// ライトから見たプロジェクション座標変換行列
m.perspective(90, 1.0, 0.1, 150, tpMatrix);
// ライトから見た座標変換行列を掛け合わせる
m.multiply(tMatrix, tpMatrix, tvpMatrix);
m.multiply(tvpMatrix, tvMatrix, tMatrix);
ライトの位置は、恒常ループの中で毎回計算するようにしています。つまり、上記のコードの中略と書かれているところから下の部分は、恒常ループの中に記述されています。ライトビューが変数 tvMatrix
に入ります。ライトプロジェクションが変数 tpMatrix
に入りますね。
テクスチャ変換用行列と、この二つの行列とを使って最終的なテクスチャ射影変換用の行列を作って tMatrix
に入れておきます。これはあとでモデルをレンダリングする際にシェーダに送りますが、その掛け合わせる順序を間違えると結果がまるで変わってきますので注意しましょう。
そのほかには、特別今までと違うことはやっていません。今回のサンプルで一番ややこしいのは行列周りの処理だと思います。それさえクリアできれば、あとは単なる点光源によるライティングを行なっているだけです。
まとめ
射影テクスチャマッピングは、やっていることはそれほど大層なことではないのですが、どうしても座標変換や行列関連の処理が難解になりがちです。
今回のサンプルは読み込んだテクスチャを射影しているだけですが、このテクニックを応用すると様々な凝った演出を行なうことが可能になります。そういう意味では、いろんな技術の基礎的な技術とも言えますね。
サンプルではいつものようにマウスによる視点の変更ができます。また先にも書いたとおり、input タグを使ってテクスチャを射影しているライトの位置を調節できるようになっています。実際に動作するものを見ながら理解を深めていただければと思います。