視差マッピング
今回のサンプルの実行結果
バンプマッピングをさらにリアルに
前回はバンプマッピングによるライティングを解説しました。
法線マップと呼ばれる特殊なテクスチャを参照しながらライティングを行うことで、平坦なポリゴンの表面上に、あたかも凹凸があるかのように見せることができるテクニックでしたね。
今回はバンプマッピングの強化版とも言える視差マッピングをやってみます。視差マッピングはその名の通り、視線や高さを考慮したバンプマッピングで、通常の法線マップのみで行なうバンプマッピングと比較すると、より明確な高低差が表れます。
以下の画像は、通常のバンプマッピングと視差マッピングとを比較したものです。左側が[ 視差マッピング ]で、右側が[ 通常のバンプマッピング ]です。
球体のちょうど赤道付近、org の o の字あたりを見ると、その効果がわかりやすいのではないでしょうか。
視差マッピングでは、高さマップと呼ばれるテクスチャを用いて、バンプマップを適用する際に高さを考慮するようにします。そうすることで、バンプマッピングよりもさらにリアルな高低のある凹凸を表現できます。
言葉だけではなかなかイメージしにくいかもしれませんが、以下の図を見ると、なんとなくイメージできるのではないでしょうか。以下の図は、斜め上方向から見たときの陰影付けが、バンプマッピングと視差マッピングでどのように違ってくるのかを表しています。
高さマップ(ハイトマップ)
視差マッピングを行なうためには、前回のバンプマッピングの際に利用した法線マップ以外に、高さマップと呼ばれるもう一つのテクスチャが必要になります。
高さマップはその名の通り、画像データに高さデータを格納したもので、通常モノクロで扱います。
黒い部分を 0 (最も低い)、白い部分は 1 (最も高い)だとすれば、この画像データ内には 0 ~ 1 の範囲のデータが格納されていることになります。これを、そのまま高さ情報として扱うわけです。
ただ、ここで勘のいい人なら気が付いたかもしれませんが、高さ情報を扱うために必要なのは 0 ~ 1 の範囲の数値を格納できるデータ領域が一つだけです。そして、前回から使っている法線マップは、XYZ で表される法線データを RGB に格納した画像データでした。
法線マップは RGB までの領域を使いますが、アルファ値については無視しているというか使っていませんね。そこで、わざわざ高さマップを別の画像として用意することをしなくても、この空いているアルファ値の領域を高さマップ用のデータ領域として使ってしまうことで、テクスチャ用の画像を一枚に集約できます。
今回はテクスチャ用の画像として、法線マップと高さマップを一応別々に用意してあります。ただ、法線マップのアルファ値に高さ情報を入れてしまえば、テクスチャ用の画像は一枚で済みます。フォトレタッチソフトなどを使えば比較的簡単にそういった処理を行うことも可能です。リソースを節約したい場合には、そのような手法を使うのもいいでしょう。
シェーダの記述
視差マッピングは、基本的にはバンプマッピングを踏襲したコードで実装できます。変更すべき点は、高さを考慮して参照する法線マップの座標をずらす処理を追加することです。
法線マップへの参照を修正することになるので、必然的に変更点があるのはフラグメントシェーダということになります。ここではそのコードだけを抜粋して見てみましょう。
フラグメントシェーダ
precision mediump float;
uniform sampler2D texture0; // normal map
uniform sampler2D texture1; // height map
uniform float height;
varying vec4 vColor;
varying vec2 vTextureCoord;
varying vec3 vEyeDirection;
varying vec3 vLightDirection;
void main(void){
vec3 light = normalize(vLightDirection);
vec3 eye = normalize(vEyeDirection);
float hScale = texture2D(texture1, vTextureCoord).r * height;
vec2 hTexCoord = vTextureCoord - hScale * eye.xy;
vec3 mNormal = (texture2D(texture0, hTexCoord) * 2.0 - 1.0).rgb;
vec3 halfLE = normalize(light + eye);
float diffuse = clamp(dot(mNormal, light), 0.1, 1.0);
float specular = pow(clamp(dot(mNormal, halfLE), 0.0, 1.0), 100.0);
vec4 destColor = vColor * vec4(vec3(diffuse), 1.0) + vec4(vec3(specular), 1.0);
gl_FragColor = destColor;
}
まず今回のシェーダでは uniform 変数として二つのテクスチャユニット番号を受け取れるようになっています。一つ目の texture0
という uniform 変数で法線マップのユニット番号を受け取り、二つ目の texture1
という uniform 変数で高さマップのユニット番号を受け取ります。
さらに、メインプログラムのほうから高さに対して適用されるスケール値を受け取ります。これは uniform 変数 height
に入ってきます。
さて、それでは main
関数のなかを見ていきましょう。
まずは高さマップのイメージから、高さに関する情報を抜き出しているのが以下のコードですね。
高さマップからの高さの取得
float hScale = texture2D(texture1, vTextureCoord).r * height;
ここで高さマップから取得した値と、メインプログラムから入ってきた高さのスケール値が掛け合わされ、最終的な高さ値が変数 hScale
に入ります。この高さ値を使って法線マップへの参照点をずらします。
実際にその参照点のずらし処理を行っているのが以下のコード。
参照点のずらしを行なっているコード
vec2 hTexCoord = vTextureCoord - hScale * eye.xy;
本来のテクスチャ座標から、高さと視点を考慮した分の値を減算してずらします。こうして得られたテクスチャ座標を使ってバンプマッピング同様の処理を行うことで、高さと視線を考慮した視差マッピングが実現できます。
メインプログラムの修正
今回のサンプルでは、視差マッピングと通常のバンプマッピングとの違いをわかりやすくするために、HTML 内に range タイプの input タグを設置しています。プログラムからこの値をリアルタイムに参照しながら、シェーダに送る高さのスケール値を変更しています。
HTML からの高さのスケール値の取得
// エレメントへの参照を取得
var eRange = document.getElementById('range');
(中略)
// 恒常ループ
(function(){
(中略)
// エレメントから高さ情報を取得
var hScale = eRange.value / 10000;
(中略)
// エレメントから取得した高さのスケール値をシェーダに送る
gl.uniform1f(uniLocation[7], hScale);
// 描画
gl.drawElements(gl.TRIANGLES, sphereData.i.length, gl.UNSIGNED_SHORT, 0);
// コンテキストの再描画
gl.flush();
// ループのために再帰呼び出し
setTimeout(arguments.callee, 1000 / 30);
})();
あらかじめ変数に input タグへの参照を取得しておき、恒常ループの中でその値を参照しながら、シェーダに送るデータをリアルタイムに変更するようにしているわけですね。
ちなみに、input タグの value
プロパティは 0 ~ 100 の範囲をとるように設定してあります。つまり、上のコードをみるとわかると思いますが、シェーダに送られる高さのスケール値の範囲は 0 ~ 0.01 という非常に狭い範囲で変化することになりますね。
このスケール値をもっと広い範囲で変化するようにしてしまうと、視差マッピングとは全く違う効果が起こります。まぁ、興味のある方はご自身で試してみることをオススメします。
基本的にメインの javascript プログラムへの変更点はそんなに多くありません。前回のバンプマッピングのサンプルに、テクスチャを新たに読み込むコードや、追加した uniform 変数を正しくプッシュするコードを追加するだけです。
まとめ
さて視差マッピング、いかがでしたか。
正直なところ、バンプマッピングと比較してものすごく大きな変化があるものでもないので、地味と言えば地味な今回のテクニック。ただ、テクスチャ座標をずらすという発想がそもそも面白いですよね。
こういうことを最初に考え出した人はすごいなぁと思ってしまいます。
サンプルへのリンクはいつものように以下にあります。実際に動作するサンプルを見たほうが、今回は効果を実感しやすいと思います。