キューブ環境マッピング
今回のサンプルの実行結果
環境マッピング
前回は高さマップと呼ばれる特殊なテクスチャを用いて視差マッピングを行ないました。
バンプマッピングと併用することで、さらにリアルな照明効果を得られる視差マッピングは、テクスチャ座標をずらすというなんとも面白い発想によって実現されています。バンプマッピングと比べてもそれほど難しくありませんので、状況に応じて活用していきましょう。
さて、今回はキューブ環境マッピングをやります。
そもそも、環境マッピングにはいろいろな手法があり、丸型の特殊なテクスチャを利用して行なう環境マッピングはスフィア環境マッピングと呼ばれます。今回取り組むキューブ環境マッピングは WebGL が最初からサポートしている環境マッピングで比較的簡単に実装できます。
環境マッピングを施した場合、レンダリングされるモデルは鏡や磨きぬかれた金属のように、周囲の景色を鮮明に映し出します。環境マッピングを応用することで、ガラスのように透明な(あるいはそのように見える)モデルを描画したりすることも可能です。環境マッピング系の処理は見た目が非常に面白いので、是非習得していただければと思います。
キューブマップテクスチャ
キューブ環境マッピングは、レンダリングされるモデルやカメラが、一つの大きな箱のなかに収まっている状態をシミュレートします。箱の中に収まっている状態を想定した処理なので、キューブ環境マッピングと呼ばれるわけですね。
モデルを構成する頂点が持つ位置や法線、さらにカメラが置かれている座標、これらのパラメータを考慮しながら参照するテクスチャとそのテクスチャ座標を操作することで、まるで金属のような質感を演出することができます。
キューブ環境マッピングを行なうためには、キューブマップテクスチャと呼ばれる特殊なテクスチャを用います。これは合計 6 枚のイメージデータからなる世界の展開図のようなテクスチャです。
全部で 6 枚のイメージが、世界の各方向を映すような形で繋がっているのがわかると思います。キューブマップテクスチャを生成するには、合計 6 枚の画像データをあらかじめ用意しておかなくてはなりません。
今回は以下の画像データをキューブマップ用として使います。
右方向(positive X)用画像
左方向(negative X)用画像
上方向(positive Y)用画像
下方向(negative Y)用画像
後方向(positive Z)用画像
前方向(negative Z)用画像
微妙に画像の作り方が下手で境界部分の色にずれがあったりもしますが、まぁそこは大目に見てください。
positive とか negative というのはそのままプラス方向かマイナス方向かを表しています。これは右手座標系(奥に行くほど Z 値がマイナスになる)の基本を考えれば直感的にわかると思います。
わかりやすく連結した状態でそれぞれの方向を書き加えると以下のようになりますね。
このような画像をあらかじめ用意することができたら、これらを利用してキューブマップテクスチャをプログラム内で準備します。
一つ注意しなければならないのは、6 枚の画像を使うといっても、テクスチャオブジェクトが 6 個必要になるということではありません。キューブマップテクスチャはオブジェクトとしては一つのテクスチャオブジェクトです。その一つのテクスチャに、順番に 6 個の画像を割り当ててやることでキューブマップテクスチャが完成します。
今までのテクスチャの生成でやってきたのと同じように、キューブマップテクスチャを生成する際にも画像イメージのロード完了を加味しなくてはなりません。そこで今回は、キューブマップ用の専用関数を用意しました。
キューブマップ生成用関数
// キューブマップテクスチャを生成する関数
function create_cube_texture(source, target){
// インスタンス用の配列
var cImg = new Array();
for(var i = 0; i < source.length; i++){
// インスタンスの生成
cImg[i] = new cubeMapImage();
// イメージオブジェクトのソースを指定
cImg[i].data.src = source[i];
}
// キューブマップ用イメージのコンストラクタ
function cubeMapImage(){
// イメージオブジェクトを格納
this.data = new Image();
// イメージロードをトリガーにする
this.data.onload = function(){
// プロパティを真にする
this.imageDataLoaded = true;
// チェック関数を呼び出す
checkLoaded();
};
}
// イメージロード済みかチェックする関数
function checkLoaded(){
// 全てロード済みならキューブマップを生成する関数を呼び出す
if( cImg[0].data.imageDataLoaded &&
cImg[1].data.imageDataLoaded &&
cImg[2].data.imageDataLoaded &&
cImg[3].data.imageDataLoaded &&
cImg[4].data.imageDataLoaded &&
cImg[5].data.imageDataLoaded){generateCubeMap();}
}
// キューブマップを生成する関数
function generateCubeMap(){
// テクスチャオブジェクトの生成
var tex = gl.createTexture();
// テクスチャをキューブマップとしてバインドする
gl.bindTexture(gl.TEXTURE_CUBE_MAP, tex);
// ソースを順に処理する
for(var j = 0; j < source.length; j++){
// テクスチャへイメージを適用
gl.texImage2D(target[j], 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, cImg[j].data);
}
// ミップマップを生成
gl.generateMipmap(gl.TEXTURE_CUBE_MAP);
// テクスチャパラメータの設定
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
// キューブマップテクスチャを変数に代入
cubeTexture = tex;
// テクスチャのバインドを無効化
gl.bindTexture(gl.TEXTURE_CUBE_MAP, null);
}
}
この関数の引数は二つですが、いずれも配列を事前に作っておいて渡す必要があります。第一引数はキューブマップ用の画像 URL のリスト、第二引数はターゲットと呼ばれる組み込み定数のリストです。これらは後々順番に処理することになるので、配列に格納する順序には気をつけてください。
※ターゲットについてはあとで詳しく解説します
呼び出しの記述例としては以下のような感じです。
関数の呼び出し記述例
// テクスチャ用の変数を用意
var cubeTexture = null;
// キューブマップ用イメージのソースを配列に格納
var cubeSourse = new Array( 'cube_PX.png',
'cube_PY.png',
'cube_PZ.png',
'cube_NX.png',
'cube_NY.png',
'cube_NZ.png');
// キューブマップ用のターゲットを格納する配列
var cubeTarget = new Array( gl.TEXTURE_CUBE_MAP_POSITIVE_X,
gl.TEXTURE_CUBE_MAP_POSITIVE_Y,
gl.TEXTURE_CUBE_MAP_POSITIVE_Z,
gl.TEXTURE_CUBE_MAP_NEGATIVE_X,
gl.TEXTURE_CUBE_MAP_NEGATIVE_Y,
gl.TEXTURE_CUBE_MAP_NEGATIVE_Z);
// キューブマップテクスチャの生成
create_cube_texture(cubeSourse, cubeTarget);
この create_cube_texture
関数はちょっと複雑な構造になっています。まず前提として、この関数の内部でさらに三つの関数が定義されているのがわかるでしょうか。
一つ目はコンストラクタである cubeMapImage
です。二つ目はイメージデータが全てロードされたかどうかをチェックする checkLoaded
関数、三つ目が実際にキューブテクスチャを完成させるための generateCubeMap
関数です。
流れとしてはこうです。
まず create_cube_texture
が呼び出されるとその内部でコンストラクタ cubeMapImage
によって六つのオブジェクトのインスタンスが新しく生成されます。インスタンスが生成されるときには、新しいイメージオブジェクトの生成処理と、そのイメージオブジェクトの onload
イベントへの登録が行なわれます。
コンストラクタ
// キューブマップ用イメージのコンストラクタ
function cubeMapImage(){
// イメージオブジェクトを格納
this.data = new Image();
// イメージロードをトリガーにする
this.data.onload = function(){
// プロパティを真にする
this.imageDataLoaded = true;
// チェック関数を呼び出す
checkLoaded();
};
}
インスタンスが生成された後、インスタンス内部のイメージオブジェクトに画像の URL をソースとして割り当てますが、既にインスタンス生成時に onload
イベントが登録してありますので、画像のロードが終わると同時に、それをトリガーにして登録した処理が走ります。その際には、イメージオブジェクトにロード済みかどうかを表す imageDataLoaded
という新しいプロパティを付加し、値を true
に設定します。そしてプロパティを設定したあとに呼び出されるのが二つ目の関数である checkLoaded
ですね。
ロード済みかどうかチェックする関数
// イメージロード済みかチェックする関数
function checkLoaded(){
// 全てロード済みならキューブマップを生成する関数を呼び出す
if( cImg[0].data.imageDataLoaded &&
cImg[1].data.imageDataLoaded &&
cImg[2].data.imageDataLoaded &&
cImg[3].data.imageDataLoaded &&
cImg[4].data.imageDataLoaded &&
cImg[5].data.imageDataLoaded){generateCubeMap();}
}
全てのプロパティが真であれば、それはつまり全ての画像データがロード済みであるということを表します。全ての画像がロードされた状態だと判断できた場合には、三つ目の関数で、実際にキューブマップテクスチャを生成する役割を持つ generateCubeMap
が呼び出されます。
キューブマップテクスチャを生成する関数
// キューブマップを生成する関数
function generateCubeMap(){
// テクスチャオブジェクトの生成
var tex = gl.createTexture();
// テクスチャをキューブマップとしてバインドする
gl.bindTexture(gl.TEXTURE_CUBE_MAP, tex);
// ソースを順に処理する
for(var j = 0; j < source.length; j++){
// テクスチャへイメージを適用
gl.texImage2D(target[j], 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, cImg[j].data);
}
// ミップマップを生成
gl.generateMipmap(gl.TEXTURE_CUBE_MAP);
// テクスチャパラメータの設定
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
// キューブマップテクスチャを変数に代入
cubeTexture = tex;
// テクスチャのバインドを無効化
gl.bindTexture(gl.TEXTURE_CUBE_MAP, null);
}
この generateCubeMap
関数の内部では、今まで普通のテクスチャを生成するときにやってきたのと同じように、まずは createTexture
メソッドを呼び出して空のテクスチャオブジェクトを生成します。その後、生成したテクスチャをバインドするのも同様です。
ただしここで注意があります。テクスチャをバインドする際には、それがキューブマップ用であることを WebGL に通知してやる必要があります。これには bindTexture
メソッドの第一引数に渡す定数に gl.TEXTURE_CUBE_MAP
を指定します。今まで普通のテクスチャを扱う場合には gl.TEXTURE_2D
を使っていましたが、キューブマップ用のテクスチャにはこちらを使うわけですね。
続いて、バインドしたテクスチャに 6 枚の画像データを順番に割り当てていきます。先ほども書いたように、割り当ての対象となるテクスチャオブジェクトはあくまでも一つだけです。それがキューブマップ用としてバインドされてさえいれば、WebGL は一つのテクスチャに正しく画像を適用してくれます。
ただここでちょっとした疑問が浮かびます。一つのテクスチャオブジェクトに画像を割り当てるといっても、いったいどの画像が、どの方向を表しているものなのか、WebGL はどうやって判断するのでしょうか。
この問題を解決するために、対象の画像を、キューブのどの面をターゲットとして適用するのか、組み込み定数によって指定できるようになっています。このターゲット面を指定するのが、配列として引数に渡したターゲット用の組み込み定数だったんですね。
テクスチャへ画像イメージを適用する際には、適切に、ターゲットを表す組み込み定数と対象の画像とを指定してやる必要があります。
テクスチャへの画像イメージの適用
// ソースを順に処理する
for(var j = 0; j < source.length; j++){
// テクスチャへイメージを適用
gl.texImage2D(target[j], 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, cImg[j].data);
}
バインドしてあるキューブマップ用のテクスチャに、組み込み定数を切り替えながら順番に画像イメージを適用していくわけですね。
この for
ステートメントによる繰り返し処理が終わると、一つのテクスチャオブジェクトに、合計 6 枚分の画像イメージが全て適用された状態になります。その後、ミップマップの生成やテクスチャパラメータの設定を行なうのは、今までのテクスチャ処理と同様ですね。いずれの場合にも gl.TEXTURE_CUBE_MAP
という組み込み定数を使うということにだけは注意しましょう。
ミップマップ生成とテクスチャパラメータの設定
// ミップマップを生成
gl.generateMipmap(gl.TEXTURE_CUBE_MAP);
// テクスチャパラメータの設定
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
// キューブマップテクスチャを変数に代入
cubeTexture = tex;
// テクスチャのバインドを無効化
gl.bindTexture(gl.TEXTURE_CUBE_MAP, null);
最終的には、全ての処理を完了したテクスチャオブジェクトを変数に代入し、バインドを解除して終了です。キューブマップ用のテクスチャは生成するだけでも一苦労といった感じもしますが、やっていることは二次元テクスチャのときと大差ありません。落ち着いて考えてみてください。
キューブマッピング用シェーダの記述
続いてはシェーダについて見ていきます。
キューブ環境マッピングでは、冒頭でも書いたとおり頂点の座標や法線、さらにカメラの座標が大きな役割を果たします。そもそもキューブマッピングでは大きな四角い箱の中にモデルがあると想定します。ですから、イメージとしては以下のような感じですね。
視点、つまりカメラの座標から延びるベクトルがモデルの表面に当たって反射し、その結果到達した箱の内側の座標、それをサンプリングすることでキューブ環境マッピングは行なわれます。キューブマッピング用のテクスチャには 6 枚の画像を使いました。この 6 枚ある画像のどれを参照するのか、そしてその画像のどの座標を参照するのか、全てはカメラの位置と頂点座標、頂点法線によって決定することができます。
頂点シェーダではモデル座標変換後の頂点の位置と法線の向きを処理します。計算結果はそのままフラグメントシェーダに送り、テクスチャ座標の計算に使われることになります。
頂点シェーダ
attribute vec3 position;
attribute vec3 normal;
attribute vec4 color;
uniform mat4 mMatrix;
uniform mat4 mvpMatrix;
varying vec3 vPosition;
varying vec3 vNormal;
varying vec4 vColor;
void main(void){
vPosition = (mMatrix * vec4(position, 1.0)).xyz;
vNormal = (mMatrix * vec4(normal, 0.0)).xyz;
vColor = color;
gl_Position = mvpMatrix * vec4(position, 1.0);
}
モデル座標変換行列 mMatrix
を uniform 変数として受け取り、頂点座標と法線に掛け合わせます。※モデル座標変換にスケーリングを含んでいる場合、このままでは問題が発生する可能性がありますが理解を簡単にするため今回は考慮しないことにしています。
座標変換後の頂点座標と法線、頂点色をフラグメントシェーダに送るだけなので簡単ですね。
さて続いてはフラグメントシェーダです。
フラグメントシェーダ側では、カメラの位置座標を uniform 変数として受け取ります。そのほか、キューブマップテクスチャのユニット番号と、フラグを一つ uniform 変数として受け取るようにしています。
まずはソースを見てみましょう。
フラグメントシェーダ
precision mediump float;
uniform vec3 eyePosition;
uniform samplerCube cubeTexture;
uniform bool reflection;
varying vec3 vPosition;
varying vec3 vNormal;
varying vec4 vColor;
void main(void){
vec3 ref;
if(reflection){
ref = reflect(vPosition - eyePosition, vNormal);
}else{
ref = vNormal;
}
vec4 envColor = textureCube(cubeTexture, ref);
vec4 destColor = vColor * envColor;
gl_FragColor = destColor;
}
キューブマップテクスチャはシェーダ内でも独自の型で表されます。普通の二次元テクスチャの場合とは異なり samplerCube
という型を使うのですね。型の名前はそのまんまな感じでわかりやすいと思います。
さて main
関数の中では uniform 変数として受け取ったフラグによって、処理が分岐するようになっていますね。フラグが真の場合には、GLSL のビルトイン関数である reflect
を使って何か処理を行なっています。
この reflect
というビルトイン関数は、その名が示す通りベクトルの反射を計算してくれます。頂点シェーダから送られてきた頂点のモデル座標変換後の座標位置から、カメラの座標位置を減算することで視線ベクトルを算出できます。この視線ベクトルと、同じく頂点シェーダから送られてきた法線とを使って、視線ベクトルの反射ベクトルを導き出し変数 ref
に代入しておきます。
この変数 ref
はご覧のとおり vec3
型です。この三つの次元を持つベクトルはキューブマップテクスチャからフラグメントの情報を抜き出すのに使われます。
二次元テクスチャの場合は texture2D
というビルトイン関数を使っていましたが、キューブマップテクスチャの場合には textureCube
を使います。この関数の第一引数にはテクスチャユニットの番号である samplerCube
型のデータを渡します。第二引数は vec3
型のデータを受け取るようになっており、ここに先ほど算出した反射ベクトルを渡します。戻り値は単純に RGBA を表す四つの要素を持つベクトルとして返ってきます。
さて、今回は uniform 変数として受け取ったフラグを用いて処理を分岐するようにしていますが、フラグが偽の場合には変数 ref
に座標変換後の法線がそのまま入るようになっています。
実はこのような分岐処理は、背景をレンダリングするために必要になります。
キューブマップを適用すると、キューブ型モデルを使って背景をレンダリングすることができます。要は、箱型のポリゴンモデルを用意しておき、本当にカメラやモデル一式をそのモデルですっぽりと包んでしまうのです。
この箱型のポリゴンモデルの内側に、キューブマップテクスチャをそのまま貼り付けると非常に自然な背景のレンダリングが行なえます。ただし、この背景のレンダリングを行なう際には、視線ベクトルの算出と反射ベクトルの算出は必要ありません。法線をそのままテクスチャ座標に適用するだけで、背景は正しくレンダリングされます。
これはキューブマップテクスチャからフラグメントを抜き出す textureCube
の仕組みを正しく理解すればわかります。このビルトイン関数は、6 枚分あるテクスチャのどの面を参照すればいいのか、そしてその面のどの座標を参照すればいいのか、これを三つの要素を持つベクトル一つで決定します。
反射ベクトルを使う場合には以下のような状態ですね。
小さなオレンジ色の丸が描かれているところが、反射ベクトルによって参照されるテクスチャの座標ということになります。
一方、反射ベクトルを使わない場合には次のようになります。
法線をそのまま用いることで、視線ベクトルを一切考慮しないフラグメントの参照ができるわけですね。
背景をレンダリングする際には、箱型のモデルを大きくスケーリングしてからレンダリングします。この際、その箱型のモデルの内側に置かれているカメラからの視線を考慮してしまうと、正しく背景がレンダリングできません。そこで、背景のレンダリングを行なう場合には、視線ベクトルを考慮しない法線のみでのフラグメントの参照を行なうわけです。
キューブマップテクスチャと、大きくスケーリングした箱型のモデルの法線情報、この二つを用いることで背景が正しくレンダリングできるわけですね。
javascript プログラム
キューブ環境マッピングの解説もいよいよ佳境です。メインプログラムを見ていきましょう。
javascript プログラムのほうでは、適切に頂点属性や uniform 変数を処理します。
今回は背景用に使う箱型のモデルと、球体モデル、トーラスモデルの三つのモデルデータを用意しています。また、冒頭で解説したキューブマップテクスチャを生成する関数を使って、6 枚の画像データをテクスチャ用に読み込みます。
恒常ループの中ではクォータニオンによるカメラの移動を行い、移動後のカメラのモデル空間上での座標を uniform 変数としてシェーダに送っています。
キューブマップテクスチャをシェーダにプッシュする際には、適切にキューブマップテクスチャをバインドすることに注意しましょう。今までは gl.TEXTURE_2D
でテクスチャをバインドしていましたが、ここが gl.TEXTURE_CUBE_MAP
になるわけですね。
また、背景用のキューブモデルをレンダリングする際には、フラグ( reflection
)を偽としてシェーダに送ることもポイントです。このフラグに応じてシェーダ側では反射ベクトルを使うかどうかを決定しているからですね。
あとは、必要に応じてモデル座標変換を行ないながらシェーダにデータをプッシュするだけです。概念さえ理解できれば、特殊なことはやっていませんのでソースを見れば意味がわかると思います。
クォータニオン関連の処理や、各種モデルの生成を行なっているのは当サイトオリジナルのライブラリ minMatrixb.js です。WebGL の組み込みメソッドなどではないので注意してください。
まとめ
少々長い解説になりましたが、キューブ環境マッピング、いかがでしたか。
今回登場した新しい概念としては、キューブマップテクスチャに関する部分が大半です。それ以外の部分では算術的にもそれほど難しいことはやっていません。せいぜい反射ベクトルを求めることくらいです。
WebGL の難しい部分の一つに、シェーダと javascript プログラムとの間で正しくデータをリンクさせなければならないという点がありますが、強いて言えばそこが最大の難関かもしれませんね。
キューブ環境マッピングは、その見た目が非常にインパクトのあるものなので、実装できたときの感動は大きいでしょう。若干わかりにくい部分はあるかもしれませんが、是非がんばって習得していただきたいと思います。
サンプルへのリンクはいつものように以下にあります。実際に動作するサンプルを見ると面白いと思います。